Merge pull request #940 from xtuple/tags/R1_4_5
[xtuple] / lib / enyo-x / source / views / navigator.js
1 /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
2 latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
3 trailing:true*/
4 /*global XT:true, XV:true, XM:true, _:true, enyo:true, window:true */
5
6 (function () {
7   var MODULE_MENU = 0;
8   var PANEL_MENU = 1;
9
10   /**
11     @name XV.Navigator
12     @class Contains a set of panels for navigating the app and modules within the app.<br />
13     Navigation within the app is accomplished by elements within the menu tool bar, such as history, search, the back button or logout.<br />
14         Navigation within modules in the app is accomplished with a list within the panel menu which displays the menu items for each context.<br />
15         The root menu (module menu) contains the list of modules and the logout.<br />
16         Only three menus are cached at one time.<br />
17         Layout: Collapsing Arranger.<br />
18     Use to implement the high-level container of all business object lists.<br />
19     Derived from <a href="http://enyojs.com/api/#enyo.Panels">enyo.Panels</a>.
20     @extends enyo.Panels
21     @extends XV.ListMenuManager
22    */
23   var navigator = /** @lends XV.Navigator# */{
24     name: "XV.Navigator",
25     kind: "Panels",
26     classes: "app enyo-unselectable",
27     /**
28       Published fields
29       @type {Object}
30
31       @property {Array} modules A DOM-free representation of all of the modules
32          contained in the navigator. The details of these module objects will
33          inform the creation of the panel components.
34
35       @property {Object} panelCache A hashmap of cached panels where the key is
36          the global ID of the panel and the value is the enyo panel component.
37     */
38     published: {
39       modules: [],
40       panelCache: {},
41       actions: [
42         {name: "newTabItem", label: "_openNewTab".loc(), method: "newTab", alwaysShowing: true},
43         {name: "preferencesItem", label: "_preferences".loc(), method: "openPreferencesWorkspace" },
44         {name: "myAccountItem", label: "_changePassword".loc(), method: "showMyAccount"},
45         {name: "helpItem", label: "_help".loc(), method: "showHelp", alwaysShowing: true},
46         {name: "aboutItem", label: "_about".loc(), method: "showAbout"}
47       ],
48     },
49     events: {
50       onListAdded: "",
51       onNavigatorEvent: "",
52       onNotify: "",
53       onWorkspace: ""
54     },
55     handlers: {
56       onDeleteTap: "showDeletePopup",
57       onListItemMenuTap: "showListItemMenu",
58       onMessage: "setMessageContent",
59       onParameterChange: "requery",
60       onColumnsChange: "changeLayout",
61       onItemTap: "itemTap",
62       onExportList: "exportList",
63       onHotKey: "handleHotKey",
64       onPrintList: "printList"
65     },
66     showPullout: true,
67     arrangerKind: "CollapsingArranger",
68     components: [
69       {kind: "FittableRows", classes: "left", components: [
70         {kind: "onyx.Toolbar", classes: "onyx-menu-toolbar", components: [
71           {kind: "onyx.Button", name: "backButton", content: "_logout".loc(),
72               ontap: "backTapped"},
73           {kind: "Group", name: "iconButtonGroup", tag: null, components: [
74             {kind: "XV.IconButton", name: "historyIconButton",
75                src: "/assets/menu-icon-bookmark.png",
76                ontap: "showHistory", content: "_history".loc()},
77             {kind: "XV.IconButton", name: "searchIconButton",
78                src: "/assets/menu-icon-search.png",
79                ontap: "showParameters", content: "_advancedSearch".loc(), showing: false}
80           ]},
81           {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "actionSelected", components: [
82             {kind: "XV.IconButton", src: "/assets/menu-icon-gear.png",
83                                                         content: "_actions".loc(), name: "actionButton"},
84             {kind: "onyx.Menu", name: "actionMenu"}
85           ]},
86           {kind: "onyx.Popup", name: "aboutPopup", centered: true,
87             modal: true, floating: true, scrim: true, components: [
88             {content: "Copyright xTuple 2013" },
89             {name: "aboutVersion", allowHtml: true },
90             {kind: "onyx.Button", content: "_ok".loc(), ontap: "closeAboutPopup"}
91           ]}
92         ]},
93         {name: "loginInfo", content: "", classes: "xv-navigator-header"},
94         {name: "menuPanels", kind: "Panels", draggable: false, fit: true,
95           margin: 0, components: [
96           {name: "moduleMenu", kind: "List", touch: true,
97               onSetupItem: "setupModuleMenuItem", ontap: "menuTap",
98               components: [
99             {name: "moduleItem", classes: "item enyo-border-box"}
100           ]},
101           {name: "panelMenu", kind: "List", touch: true,
102              onSetupItem: "setupPanelMenuItem", ontap: "panelTap", components: [
103             {name: "listItem", classes: "item enyo-border-box"}
104           ]},
105           {} // Why do panels only work when there are 3+ objects?
106         ]}
107       ]},
108       {kind: "FittableRows", components: [
109                                 // the onyx-menu-toolbar class keeps the popups from being hidden
110         {kind: "onyx.MoreToolbar", name: "contentToolbar",
111           classes: "onyx-menu-toolbar", movedClass: "xv-toolbar-moved", components: [
112           {kind: "onyx.Grabber"},
113           {name: "rightLabel", style: "width: 180px"},
114           // The MoreToolbar is a FittableColumnsLayout, so this spacer takes up all available space
115           {name: "spacer", content: "", fit: true},
116           // Selectable "New" menu
117           {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "newRecord", components: [
118             {kind: "XV.IconButton", src: "/assets/menu-icon-new.png",
119               content: "_new".loc(), name: "newMenuButton", showing: false},
120             {kind: "onyx.Menu", name: "newMenu"}
121           ]},
122           // Button to initiate new workspace
123           {kind: "XV.IconButton", src: "/assets/menu-icon-new.png",
124             content: "_new".loc(), name: "newButton", ontap: "newRecord", showing: false},
125           {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "exportSelected", components: [
126             {kind: "XV.IconButton", src: "/assets/menu-icon-export.png",
127               content: "_export".loc(), name: "exportButton"},
128             {kind: "onyx.Menu", name: "exportMenu", showing: false}
129           ]},
130           {kind: "XV.IconButton", name: "sortButton", content: "_sort".loc(),
131             src: "/assets/sort-icon.png", ontap: "showSortPopup", showing: false},
132           {kind: "XV.SortPopup", name: "sortPopup", showing: false},
133           {name: "refreshButton", kind: "XV.IconButton",
134             src: "/assets/menu-icon-refresh.png", content: "_refresh".loc(),
135             ontap: "requery", showing: false},
136           {name: "search", kind: "onyx.InputDecorator",
137             showing: false, components: [
138             {name: 'searchInput', kind: "onyx.Input", style: "width: 200px;",
139               placeholder: "_search".loc(), onchange: "inputChanged"},
140             {kind: "Image", src: "/assets/search-input-search.png",
141               name: "searchJump", ontap: "jump"}
142           ]}
143         ]},
144         {name: "messageHeader", content: "", classes: ""},
145         {name: "header", content: "", classes: ""},
146         {name: "contentPanels", kind: "Panels", margin: 0, fit: true,
147           draggable: false, panelCount: 0, classes: "scroll-ios"},
148         {name: "myAccountPopup", kind: "XV.MyAccountPopup"},
149         {name: "listItemMenu", kind: "onyx.Menu", floating: true, onSelect: "listActionSelected",
150           maxHeight: 500, components: []
151         }
152       ]}
153     ],
154     /**
155       Keeps track of whether any list has already been fetched, to avoid unnecessary
156       refetching.
157      */
158     fetched: {},
159     actionSelected: function (inSender, inEvent) {
160       var index = this.$.menuPanels.getIndex(),
161         context = this,
162         action = inEvent.originator.action,
163         method = action.method || action.name;
164
165       // Determine if action is coming from a more specific context
166       if (action.context) {
167         context = this.$.contentPanels.getActive();
168       } else if (index && !action.alwaysShowing) {
169         context = this.getSelectedModule();
170       }
171
172       context[method](inSender, inEvent);
173     },
174     activate: function () {
175       this.setMenuPanel(MODULE_MENU);
176     },
177     /**
178       The back button is a logout button if you're at the root menu. Otherwise it's a
179       back button that takes you to the root menu.
180      */
181     backTapped: function () {
182       var index = this.$.menuPanels.getIndex();
183       if (index === MODULE_MENU) {
184         var inEvent = {
185           originator: this,
186           type: XM.Model.QUESTION,
187           callback: function (response) {
188             if (response.answer) {
189               XT.logout();
190             }
191           },
192           message: "_logoutConfirmation".loc()
193         };
194         this.doNotify(inEvent);
195       } else {
196         this.setHeaderContent("");
197         this.setMenuPanel(MODULE_MENU);
198       }
199     },
200     /**
201       If we're on the main menu, then use the navigator actions defined
202       in its `actions` property. If we're in a module, then use the module's
203       actions.
204     */
205     buildMenus: function () {
206       var actionMenu = this.$.actionMenu,
207         exportMenu = this.$.exportMenu,
208         newMenu = this.$.newMenu,
209         // This is the index of the active panel. All panels
210         // that have been selected from the menus have an index of
211         // greater than 0. The welcome page has an index of 0.
212         idx = this.$.menuPanels.getIndex(),
213         activePanel = this.$.contentPanels.getActive(),
214         ary = idx ? (this.getSelectedModule().actions || []).concat(this.getActions(true)) : this.getActions(),
215         actions = ary.slice(0),
216         privileges = XT.session.privileges;
217
218       // HANDLE ACTIONS
219       // reset the menu
220       actionMenu.destroyClientControls();
221
222       if (idx && activePanel.getNavigatorActions) {
223         _.each(activePanel.getNavigatorActions(), function (action) {
224           action.context = activePanel;
225           actions.push(action);
226         });
227       }
228
229       // then add whatever actions are applicable to the current context
230       _.each(actions, function (action) {
231         var name = action.name,
232           privilege = action.privilege,
233           isDisabled = privilege ? privileges.get(privilege) : false;
234         actionMenu.createComponent({
235           name: name,
236           kind: XV.MenuItem,
237           content: action.label || ("_" + name).loc(),
238           action: action,
239           disabled: isDisabled
240         });
241
242       });
243       actionMenu.render();
244       this.$.actionButton.setShowing(actions.length);
245
246       // HANDLE EXPORTS
247       actions = [];
248
249       // reset the menu
250       exportMenu.destroyClientControls();
251
252       if (idx && activePanel.getExportActions) {
253         _.each(activePanel.getExportActions(), function (action) {
254           action.context = activePanel;
255           actions.push(action);
256         });
257       }
258
259       // then add whatever actions are applicable to the current context
260       _.each(actions, function (action) {
261         var name = action.name,
262           privilege = action.privilege,
263           isDisabled = privilege ? privileges.get(privilege) : false;
264         exportMenu.createComponent({
265           name: name,
266           kind: XV.MenuItem,
267           content: action.label || ("_" + name).loc(),
268           action: action,
269           disabled: isDisabled
270         });
271       });
272       exportMenu.render();
273       this.$.exportButton.setShowing(actions.length);
274
275       // HANDLE SORT BUTTON
276       // if the activepanel is a list of some kind, show the button
277       if (activePanel.kindClasses) {
278         this.$.sortButton.setShowing(activePanel.kindClasses.indexOf("list") !== -1);
279       } else {
280         this.$.sortButton.setShowing(false);
281       }
282
283       // HANDLE NEW
284       actions = [];
285
286       // reset the menu
287       newMenu.destroyClientControls();
288
289       if (idx && activePanel.getNewActions) {
290         _.each(activePanel.getNewActions(), function (action) {
291           action.context = activePanel;
292           actions.push(action);
293         });
294       }
295
296       // then add whatever actions are applicable to the current context
297       _.each(actions, function (action) {
298         newMenu.createComponent({
299           name: action.name,
300           kind: XV.MenuItem,
301           content: action.label || ("_" + action.name).loc(),
302           // this item is the payload from the menu item
303           // that can be handled differently depending on the panel.
304           item: action.item
305         });
306       });
307       newMenu.render();
308     },
309     /**
310       If there is a parameter widget, send the current list to the
311         layout form to build the list of columns.
312     */
313     buildLayout: function () {
314       var list = this.$.contentPanels.getActive(),
315         parameterWidget = XT.app ? XT.app.$.pullout.getParameterWidget(list.name) : null;
316       if (parameterWidget && parameterWidget.showLayout) {
317         parameterWidget.buildColumnList(list);
318       }
319     },
320     /**
321       The navigator only keeps three panels in the DOM at a time. Anything extra panels
322       will be periodically cached into the panelCache published field and removed from the DOM.
323     */
324     cachePanels: function () {
325       var contentPanels = this.$.contentPanels,
326         panelToCache,
327         globalIndex,
328         pertinentModule,
329         panelReference,
330         findPanel = function (panel) {
331           return panel.index === globalIndex;
332         },
333         findModule = function (module) {
334           var panel = _.find(module.panels, findPanel);
335           return panel !== undefined;
336         };
337
338       while (contentPanels.children.length > 3) {
339         panelToCache = contentPanels.children[0];
340         globalIndex = panelToCache.index;
341
342         // Panels are abstractly referenced in this.getModules().
343         // Find the abstract panel of the panelToCache
344         // XXX this would be cleaner if we kept a backwards reference
345         // from the panel to its containing module (and index therein)
346         pertinentModule = _.find(this.getModules(), findModule);
347         panelReference = _.find(pertinentModule.panels, findPanel);
348
349         contentPanels.removeChild(panelToCache);
350         // only render the most recent (i.e. active) child
351         contentPanels.children[2].render();
352         panelReference.status = "cached";
353         this.getPanelCache()[globalIndex] = panelToCache;
354       }
355     },
356     /**
357       When a new column value is selected in the layout panel,
358       this value replaces the old attribute value in the list
359       field.
360     */
361     changeLayout: function (inSender, inEvent) {
362       var newValue = inEvent.value ? inEvent.value : "",
363         order = inEvent.order,
364         list = this.$.contentPanels.getActive(),
365         // get the current list of attribute kinds
366         currentColumns = _.filter(list.$, function (item) {
367           return item.kind === "XV.ListAttr";
368         });
369
370       // When we get to the current selected list attribute,
371       // set the new value in place of the old attribute
372       for (var i = 0; i < currentColumns.length; i++) {
373         var col = currentColumns[i];
374         if (order === (i + 1)) {
375           col.setAttr(newValue);
376         }
377       }
378
379       // requery this list with the new attribute values
380       this.requery();
381
382       // rebuild the tree of columns
383       this.buildLayout();
384
385       return true; // stop right here
386     },
387     clearMessage: function () {
388       this.$.messageHeader.setContent("");
389       this.$.messageHeader.setClasses("");
390     },
391     closeAboutPopup: function () {
392       this.$.aboutPopup.hide();
393     },
394     create: function () {
395       this.inherited(arguments);
396       var that = this,
397         callback = function () {
398           that.buildMenus();
399         };
400
401       // If not everything is loaded yet, come back to it later
402       if (!XT.session || !XT.session.privileges) {
403         XT.getStartupManager().registerCallback(callback);
404       } else {
405         callback();
406       }
407     },
408     exportSelected: function (inSender, inEvent) {
409       var index = this.$.menuPanels.getIndex(),
410         context = this,
411         action = inEvent.originator.action,
412         method = action.method || action.name;
413
414       action.context[method](inSender, inEvent);
415     },
416     getActions: function (alwaysShowingOnly) {
417       var actions = this.actions;
418       if (alwaysShowingOnly) {
419         actions = _.filter(actions, function (action) {
420           return action.alwaysShowing === true;
421         });
422       }
423       return actions;
424     },
425     getSelectedModule: function () {
426       return this._selectedModule;
427     },
428     /**
429       Exports the contents of a list to CSV. Note that it will export the entire
430       list, not just the part that's been lazy-loaded. Of course, it will apply
431       the filter criteria as selected. Goes to the server for this.
432       Avoids websockets or AJAX because the server will prompt the browser to download
433       the file by setting the Content-Type of the response, which is not possible with
434       those technologies.
435      */
436     exportList: function (inSender, inEvent) {
437       this.openExportTab('export');
438       return true;
439     },
440     newTab: function () {
441       window.open(XT.getOrganizationPath() + '/app', '_newtab');
442     },
443     openPreferencesWorkspace: function () {
444       this.doWorkspace({workspace: "XV.UserPreferenceWorkspace", id: false});
445     },
446     printList: function (inSender, inEvent) {
447       this.openExportTab('report');
448       return true;
449     },
450     openExportTab: function (routeName) {
451       var list = this.$.contentPanels.getActive(),
452         recordType = list.value.model.prototype.recordType,
453         query = JSON.parse(JSON.stringify(list.getQuery())); // clone
454
455       delete query.rowLimit;
456       delete query.rowOffset;
457
458       // sending the locale information back over the wire saves a call to the db
459       window.open(XT.getOrganizationPath() +
460         '/%@?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@}'
461         .f(routeName,
462           recordType.prefix(),
463           recordType.suffix(),
464           JSON.stringify(query),
465           JSON.stringify(XT.locale.culture)),
466         '_newtab');
467     },
468     showSortPopup: function (inSender, inEvent) {
469       this.$.sortPopup.setList(this.$.contentPanels.getActive());
470       this.$.sortPopup.setNav(this);
471       this.$.sortPopup.setPickerStrings();
472       this.$.sortPopup.show();
473     },
474
475     /**
476       Fetch a list.
477      */
478     fetch: function (options) {
479       options = options ? _.clone(options) : {};
480       var list = this.$.contentPanels.getActive(),
481         name = list ? list.name : "",
482         query,
483         input,
484         parameterWidget,
485         parameters,
486         filterDescription;
487
488       // in order to continue to the fetch, this needs to be a list
489       // or a dashboard
490       if (!(list instanceof XV.List) && !(list instanceof XV.Dashboard)) { return; }
491
492       query = list.getQuery() || {};
493       input = this.$.searchInput.getValue();
494
495       // if the "list" doesn't allow dynamic searching, skip this
496       // Dashboards have an allowFilter of false
497       if (list.allowFilter) {
498         parameterWidget = XT.app ? XT.app.$.pullout.getParameterWidget(name) : null;
499         parameters = parameterWidget ? parameterWidget.getParameters() : [];
500         options.showMore = _.isBoolean(options.showMore) ?
501           options.showMore : false;
502
503         // Get information from filters and set description
504         filterDescription = this.formatQuery(parameterWidget ? parameterWidget.getSelectedValues() : null, input);
505         list.setFilterDescription(filterDescription);
506         this.setHeaderContent(filterDescription);
507
508         delete query.parameters;
509         // Build parameters
510         if (input || parameters.length) {
511           query.parameters = [];
512
513           // Input search parameters
514           if (input) {
515             query.parameters.push({
516               attribute: list.getSearchableAttributes(),
517               operator: 'MATCHES',
518               value: this.$.searchInput.getValue()
519             });
520           }
521
522           // Advanced parameters
523           if (parameters) {
524             query.parameters = query.parameters.concat(parameters);
525           }
526         }
527
528         // if there is a parameter widget for this list, build the columns
529         if (parameterWidget && parameterWidget.showLayout) {
530           this.buildLayout();
531         }
532       }
533
534       list.setQuery(query);
535       list.fetch(options);
536     },
537     formatQuery: function (advancedSearch, simpleSearch) {
538       var key,
539         formattedQuery = "";
540
541       for (key in advancedSearch) {
542         formattedQuery += (key + ": " + advancedSearch[key] + ", ");
543       }
544
545       if (simpleSearch && formattedQuery) {
546         formattedQuery += "_match".loc() + ": " + simpleSearch;
547       } else if (simpleSearch) {
548         formattedQuery += simpleSearch;
549       }
550
551       if (formattedQuery) {
552         formattedQuery = "_filterBy".loc() + ": " + formattedQuery;
553       }
554
555       if (formattedQuery.lastIndexOf(", ") + 2 === formattedQuery.length) {
556         // chop off trailing comma
557         formattedQuery = formattedQuery.substring(0, formattedQuery.length - 2);
558       }
559
560       return formattedQuery;
561     },
562     handleHotKey: function (inSender, inEvent) {
563       var destinationIndex,
564         isWelcome = this.getSelectedModule().name === 'welcome',
565         currentIndex,
566         keyCode = inEvent.keyCode;
567
568       // numbers navigate to the nth menu option
569       if (keyCode >= 49 && keyCode <= 57) {
570         destinationIndex = keyCode - 49;
571         if (isWelcome) {
572           this.setModule(Math.min(destinationIndex, this.getModules().length - 1));
573         } else {
574           this.setContentPanel(Math.min(destinationIndex, this.getSelectedModule().panels.length - 1));
575         }
576         return;
577
578       } else if (!isWelcome) {
579         currentIndex = this.$.panelMenu.getSelection().lastSelected;
580         if (keyCode === 38) {
581           this.setContentPanel(Math.max(currentIndex - 1, 0));
582           return;
583         } else if (keyCode === 40) {
584           this.setContentPanel(Math.min(currentIndex + 1, this.getSelectedModule().panels.length - 1));
585           return;
586         }
587       }
588
589       switch(String.fromCharCode(keyCode)) {
590       case 'A':
591         this.showParameters();
592         break;
593       case 'B':
594         this.backTapped();
595         break;
596       case 'H':
597         this.showHelp();
598         break;
599       case 'N':
600         this.newRecord({}, {originator: {}});
601         break;
602       case 'R':
603         this.requery();
604         break;
605       }
606     },
607     inputChanged: function (inSender, inEvent) {
608       this.fetched = {};
609       this.fetch();
610     },
611     /**
612       Drills down into a workspace if a user clicks a list item.
613      */
614     itemTap: function (inSender, inEvent) {
615       var list = inEvent.list,
616         workspace = list ? list.getWorkspace() : null,
617         model = list.getModel(inEvent.index),
618         canNotRead = model.couldRead ? !model.couldRead() : !model.getClass().canRead(),
619         id = model && model.id ? model.id : false;
620
621       // Check privileges first
622       if (canNotRead) {
623         this.showError("_insufficientViewPrivileges".loc());
624         return true;
625       }
626
627       // Bubble requset for workspace view, including the model id payload
628       if (workspace) { this.doWorkspace({workspace: workspace, id: id}); }
629       return true;
630     },
631     jump: function () {
632       var list = this.$.contentPanels.getActive(),
633          workspace = list ? list.getWorkspace() : null,
634          Klass = list.getValue().model,
635          upper = this._getModelProperty(Klass, 'enforceUpperKey'),
636          input = this.$.searchInput.getValue(),
637          that = this,
638          options = {},
639          key = this._getModelProperty(Klass, 'documentKey'),
640          model,
641          attrs = {};
642       if (this._busy  || !input || !key) { return; }
643       this._busy = true;
644
645       // First find a matching id
646       options.success = function (id) {
647         var options = {};
648         if (id) {
649
650           // Next fetch the model, see if we have privs
651           options.success = function () {
652             var canNotRead = model.couldRead ?
653               !model.couldRead() : !model.getClass().canRead();
654
655             // Check privileges first
656             if (canNotRead) {
657               this.showError("_insufficientViewPrivileges".loc());
658             } else {
659
660               // Bubble requset for workspace view, including the model id payload
661               if (workspace) { that.doWorkspace({workspace: workspace, id: id}); }
662             }
663             that._busy = false;
664           };
665           attrs[Klass.prototype.idAttribute] = id;
666           model = Klass.findOrCreate(attrs);
667           model.fetch(options);
668         } else {
669           that.showError("_noDocumentFound".loc());
670           that.$.searchInput.clear();
671           that._busy = false;
672         }
673       };
674       input = upper ? input.toUpperCase() : input;
675       Klass.findExisting(key, input, options);
676     },
677     loginInfo: function () {
678       return this.$.loginInfo;
679     },
680     /**
681       Handles additive changes only
682     */
683     modulesChanged: function () {
684       var modules = this.getModules() || [],
685         existingModules = this._modules || [],
686         existingModule,
687         existingPanel,
688         panels,
689         panel,
690         i,
691         n,
692         findExistingModule = function (name) {
693           return _.find(existingModules, function (module) {
694             return module.name === name;
695           });
696         },
697         findExistingPanel = function (panels, name) {
698           return _.find(panels, function (panel) {
699             return panel.name === name;
700           });
701         };
702
703       // Build panels
704       for (i = 0; i < modules.length; i++) {
705         panels = modules[i].panels || [];
706         existingModule = findExistingModule(modules[i].name);
707         for (n = 0; n < panels.length; n++) {
708
709           // If the panel already exists, move on
710           if (existingModule) {
711             existingPanel = findExistingPanel(existingModule.panels, panels[n].name);
712             if (existingPanel) { continue; }
713           }
714
715           // Keep track of where this panel is being placed for later reference
716           panels[n].index = this.$.contentPanels.panelCount++;
717
718           // XXX try this: only create the first three
719           if (panels[n].index < 3) {
720             panels[n].status = "active";
721
722             // Default behavior for Lists is toggle selections
723             // So we can perform actions on rows. If not a List
724             // this property shouldn't hurt anything
725             if (panels[n].toggleSelected === undefined) {
726               panels[n].toggleSelected = true;
727             }
728             panel = this.$.contentPanels.createComponent(panels[n]);
729             if (panel instanceof XV.List) {
730
731               // Bubble parameter widget up to pullout
732               this.doListAdded(panel);
733             }
734           } else {
735             panels[n].status = "unborn";
736           }
737         }
738       }
739       this.$.moduleMenu.setCount(modules.length);
740       // Cache a deep copy
741       this._modules = JSON.parse(JSON.stringify(modules));
742       this.render();
743     },
744     /**
745       Fired when the user clicks the "New" button. Takes the user to a workspace
746       backed by an empty object of the type displayed in the current list.
747      */
748     newRecord: function (inSender, inEvent) {
749       var list = this.$.contentPanels.getActive(),
750         workspace = list instanceof XV.List ? list.getWorkspace() : null,
751         item = inEvent.originator.item,
752         Model,
753         canCreate,
754         callback;
755
756       if (list instanceof XV.Dashboard && item) {
757         list.newRecord(item);
758         return true;
759       }
760
761       if (!list instanceof XV.List) {
762         return true;
763       }
764
765       // Check privileges
766       Model = list.getValue().model;
767       canCreate = Model.couldCreate ? Model.couldCreate() : Model.canCreate();
768       if (!canCreate) {
769         this.showError("_insufficientCreatePrivileges".loc());
770         return true;
771       }
772
773       if (workspace) {
774         this.doWorkspace({
775           workspace: workspace
776         });
777       }
778
779       // In addition to preventing Enyo event propagation,
780       // we need to prevent propagation of DOM events to support
781       // mobile browsers and long button clicks
782       // check to make sure this is a button click before calling inEvent function
783       if (inEvent && inEvent.preventDefault) {
784         inEvent.preventDefault();
785       }
786       return true;
787     },
788     popupHidden: function (inSender, inEvent) {
789       if (!this._popupDone) {
790         inEvent.originator.show();
791       }
792     },
793     requery: function (inSender, inEvent) {
794       this.fetch();
795     },
796     /**
797       Determines whether the advanced search or the history icon (or neither) is
798       lit.
799      */
800     setActiveIconButton: function (buttonName) {
801       var activeIconButton = null;
802       // Null deactivates both
803       if (buttonName === 'search') {
804         activeIconButton = this.$.searchIconButton;
805       } else if (buttonName === 'history') {
806         activeIconButton = this.$.historyIconButton;
807       }
808       this.$.iconButtonGroup.setActive(activeIconButton);
809     },
810     /**
811       Renders a list and performs all the necessary auxilliary work such as hiding/showing
812       the advanced search icon if appropriate. Called when a user chooses a menu item.
813      */
814     setContentPanel: function (index) {
815       var contentPanels = this.$.contentPanels,
816         module = this.getSelectedModule(),
817         panelIndex = module && module.panels ? module.panels[index].index : -1,
818         panelStatus = module && module.panels ? module.panels[index].status : 'unknown',
819         panel,
820         label,
821         collection,
822         model,
823         canNotCreate = true;
824
825       this.clearMessage();
826       if (panelStatus === 'active') {
827         panel = _.find(contentPanels.children, function (child) {
828           return child.index === panelIndex;
829         });
830       } else if (panelStatus === 'unborn') {
831         // panel exists but has not been rendered. Render it.
832         module.panels[index].status = 'active';
833
834         // Default behavior for Lists is toggle selections
835         // So we can perform actions on rows. If not a List
836         // this property shouldn't hurt anything
837         if (module.panels[index].toggleSelected === undefined) {
838           module.panels[index].toggleSelected = true;
839         }
840         panel = contentPanels.createComponent(module.panels[index]);
841         panel.render();
842         if (panel instanceof XV.List) {
843
844           // Bubble parameter widget up to pullout
845           this.doListAdded(panel);
846         }
847
848       } else if (panelStatus === 'cached') {
849         module.panels[index].status = 'active';
850         panel = this.panelCache[panelIndex];
851         contentPanels.addChild(panel);
852         panel.node = undefined; // new to enyo2.2! wipe out the node so that it can get re-rendered fresh
853         panel.render();
854
855       } else {
856         XT.error("Don't know what to do with this panel status");
857       }
858
859       // Mobile device view
860       if (enyo.Panels.isScreenNarrow()) {
861         this.next();
862       }
863
864       // If we're already here, bail
865       if (contentPanels.index === this.$.contentPanels.indexOfChild(panel)) {
866         return;
867       }
868
869       // cache any extraneous content panels
870       this.cachePanels();
871
872       label = panel && panel.label ? panel.label : "";
873       collection = panel && panel.getCollection ? XT.getObjectByName(panel.getCollection()) : false;
874
875       if (!panel) { return; }
876
877       // Make sure the advanced search icon is visible iff there is an advanced
878       // search for this list
879       if (panel.parameterWidget) {
880         this.$.searchIconButton.setShowing(true);
881       } else {
882         this.$.searchIconButton.setShowing(false);
883       }
884       this.doNavigatorEvent({name: panel.name, show: false});
885
886       // Handle new button
887       this.$.newButton.setShowing(panel.canAddNew && !panel.newActions);
888       this.$.newMenuButton.setShowing(panel.canAddNew && panel.newActions);
889
890       if (panel.canAddNew && collection) {
891         // Check 'couldCreate' first in case it's an info model.
892         model = collection.prototype.model;
893         canNotCreate = model.prototype.couldCreate ? !model.prototype.couldCreate() : !model.canCreate();
894       }
895       this.$.newButton.setDisabled(canNotCreate);
896
897       // Select panelMenu
898       if (!this.$.panelMenu.isSelected(index)) {
899         this.$.panelMenu.select(index);
900       }
901
902       // Select list
903       contentPanels.setIndex(this.$.contentPanels.indexOfChild(panel));
904
905       this.$.rightLabel.setContent(label);
906       if (panel.getFilterDescription) {
907         this.setHeaderContent(panel.getFilterDescription());
908       }
909       if (panel.fetch && !this.fetched[panelIndex]) {
910         this.fetch();
911         this.fetched[panelIndex] = true;
912       }
913
914       this.buildMenus();
915       this.$.contentToolbar.resized();
916     },
917
918     /**
919       The header content typically describes to the user the particular query filter in effect.
920      */
921     setHeaderContent: function (content) {
922       this.$.header.setContent(content);
923       if (content !== "") {
924         this.$.header.setClasses("xv-navigator-header");
925       } else {
926         this.$.header.setClasses("");
927       }
928     },
929     setMenuPanel: function (index) {
930       var label = index ? "_back".loc() : "_logout".loc();
931       this.$.menuPanels.setIndex(index);
932                         // only automatically select the first screen if it's the module menu
933       if (!enyo.Panels.isScreenNarrow()) {
934         this.$.menuPanels.getActive().select(0);
935         this.setContentPanel(0);
936       }
937       this.$.backButton.setContent(label);
938       this.$.refreshButton.setShowing(index);
939       this.$.search.setShowing(index);
940       this.$.contentToolbar.resized();
941     },
942     setMessageContent: function (inSender, inEvent) {
943       var content = inEvent.message;
944
945       this.$.messageHeader.setContent(content);
946       if (content !== "") {
947         this.$.messageHeader.setClasses("xv-navigator-header");
948       } else {
949         this.$.messageHeader.setClasses("");
950       }
951     },
952     setModule: function (index) {
953       var module = this.getModules()[index],
954         panels = module.panels || [],
955         hasSubmenu = module.hasSubmenu !== false && panels.length;
956       if (module !== this._selectedModule || enyo.Panels.isScreenNarrow()) {
957         this._selectedModule = module;
958         if (hasSubmenu) {
959           this.$.panelMenu.setCount(panels.length);
960           this.$.panelMenu.render();
961           this.setMenuPanel(PANEL_MENU);
962         } else {
963           // if no submenus, treat lke a panel
964           this.setContentPanel(index);
965         }
966       }
967     },
968     setModules: function (modules) {
969       this.modules = modules;
970       this.modulesChanged();
971     },
972     /**
973       Renders a list of modules from the root menu.
974      */
975     setupModuleMenuItem: function (inSender, inEvent) {
976       var index = inEvent.index,
977         label = this.modules[index].label,
978         isSelected = inSender.isSelected(index);
979       this.$.moduleItem.setContent(label);
980       this.$.moduleItem.addRemoveClass("onyx-selected", isSelected);
981       if (isSelected) { this.setModule(index); }
982     },
983     /**
984       Renders the leftbar list of objects within a given module. This function
985       is also called when a leftbar item is tapped, per enyo's List conventions.
986      */
987     setupPanelMenuItem: function (inSender, inEvent) {
988       var module = this.getSelectedModule(),
989         index = inEvent.index,
990         isSelected = inSender.isSelected(index),
991         panel = module.panels[index],
992         name = panel && panel.name ? module.panels[index].name : "",
993         // peek inside the kind to see what the label should be
994         kind = panel && panel.kind ? XT.getObjectByName(panel.kind) : null,
995         label = kind && kind.prototype.label ? kind.prototype.label : "",
996         shortKindName;
997
998       if (!label && kind && kind.prototype.determineLabel) {
999         // some of these lists have labels that are dynamically computed,
1000         // so we can't rely on their being statically defined. We have to
1001         // compute them in the same way that their create() method would.
1002         shortKindName = panel.kind.substring(0, panel.kind.length - 4).substring(3);
1003         label = kind.prototype.determineLabel(shortKindName);
1004
1005       } else if (!label) {
1006         label = panel ? panel.label || name : name;
1007       }
1008
1009       this.$.listItem.setContent(label);
1010       this.$.listItem.addRemoveClass("onyx-selected", isSelected);
1011     },
1012
1013     /**
1014       This function is called when a panel is selected. If the selection is valid,
1015       then the content panel is set for that panel selection.
1016     */
1017     panelTap: function (inSender, inEvent) {
1018       var index = inEvent.index, validIndex = index || index === 0;
1019       if (validIndex && inSender.isSelected(index)) { // make sure an item in the list was clicked and is selected
1020         this.setContentPanel(index);
1021       }
1022     },
1023
1024     /**
1025       This function is called when a module is selected. If the selection is valid,
1026       then the list of panels is shown for that module.
1027     */
1028     menuTap: function (inSender, inEvent) {
1029       var validIndex = inEvent.index || inEvent.index === 0;
1030       if (validIndex) { // make sure an item in the list was clicked
1031         this.setupModuleMenuItem(inSender, inEvent);
1032       }
1033     },
1034     showAbout: function () {
1035       this.$.aboutPopup.show();
1036     },
1037     /**
1038       Error notification, using XV.ModuleContainer notify mechanism
1039      */
1040     showError: function (message) {
1041       var inEvent = {
1042         originator: this,
1043         type: XM.Model.CRITICAL,
1044         message: message
1045       };
1046       this.doNotify(inEvent);
1047     },
1048     showHelp: function () {
1049       var listName = this.$.contentPanels.getActive().name,
1050         // this will work fine starting in 1.4.6:
1051         //culture = XT.locale.culture,
1052         culture = XT.locale.culture || XT.session.locale.attributes.culture,
1053         objectName = (listName.indexOf("List") >= 0 || listName.indexOf("Page") >= 0) ?
1054           listName.substring(0, listName.length - 4) : // get rid of the word "List" or "Page"
1055           listName,
1056         pageName = objectName.decamelize().replace(/_/g, "-"),
1057         url = XT.HELP_URL_ROOT + pageName + "?culture=" + culture,
1058         panel = {name: 'help', show: true, url: url};
1059
1060       this.doNavigatorEvent(panel);
1061       //window.open(url, "_blank", "width=400,height=600");
1062     },
1063     /**
1064       Displays the history panel.
1065      */
1066     showHistory: function (inSender, inEvent) {
1067       var panel = {name: 'history', show: true};
1068       this.doNavigatorEvent(panel);
1069     },
1070     /**
1071       Displays the advanced search panel.
1072      */
1073     showParameters: function (inSender, inEvent) {
1074       var list = this.$.contentPanels.getActive();
1075       this.doNavigatorEvent({name: list.name, show: true});
1076     },
1077     /**
1078       Displays the My Account popup.
1079      */
1080     showMyAccount: function (inSender, inEvent) {
1081       this.$.myAccountPopup.show();
1082     },
1083     /** @private */
1084     _getModelProperty: function (Klass, prop) {
1085       var ret = false;
1086       // Get the key if it's a document model
1087       if (Klass.prototype[prop]) {
1088         ret = Klass.prototype[prop];
1089
1090       // Hopefully it's an info model
1091       } else if (Klass.prototype.editableModel) {
1092         Klass = XT.getObjectByName(Klass.prototype.editableModel);
1093         ret = Klass.prototype[prop];
1094       }
1095
1096       return ret;
1097     }
1098   };
1099
1100   enyo.mixin(navigator, XV.ListMenuManagerMixin);
1101   enyo.kind(navigator);
1102
1103 }());