1 /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
2 latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
4 /*global XT:true, XV:true, XM:true, _:true, enyo:true, window:true */
12 @class Contains a set of panels for navigating the app and modules
14 Navigation within the app is accomplished by elements within the menu
15 tool bar, such as history, search, the back button or logout.<br/>
16 Navigation within modules in the app is accomplished with a list within
17 the panel menu which displays the menu items for each context.<br/>
18 The root menu (module menu) contains the list of modules and the logout.<br/>
19 Only three menus are cached at one time.<br />
20 Layout: Collapsing Arranger.<br />
21 Use to implement the high-level container of all business object lists.<br/>
22 Derived from <a href="http://enyojs.com/api/#enyo.Panels">enyo.Panels</a>.
24 @extends XV.ListMenuManager
26 var navigator = /** @lends XV.Navigator# */{
28 kind: "XV.GridPanels",
33 @property {Array} modules A DOM-free representation of all of the modules
34 contained in the navigator. The details of these module objects will
35 inform the creation of the panel components.
37 @property {Object} panelCache A hashmap of cached panels where the key is
38 the global ID of the panel and the value is the enyo panel component.
44 {name: "newTabItem", label: "_openNewTab".loc(), method: "newTab", alwaysShowing: true},
45 {name: "preferencesItem", label: "_preferences".loc(), method: "openPreferencesWorkspace" },
46 {name: "myAccountItem", label: "_changePassword".loc(), method: "showMyAccount"},
47 {name: "helpItem", label: "_help".loc(), method: "showHelp", alwaysShowing: true},
48 {name: "aboutItem", label: "_about".loc(), method: "showAbout"}
58 onDeleteTap: "showDeletePopup",
59 onListItemMenuTap: "showListItemMenu",
60 onMessage: "setMessageContent",
61 onParameterChange: "requery",
62 onColumnsChange: "changeLayout",
64 onExportList: "exportList",
65 onHotKey: "handleHotKey",
66 onPrintList: "printList",
67 onPrintSelectList: "printSelectList",
68 onReportList: "reportList",
72 {kind: "FittableRows", name: "nav-menu", classes: "left", components: [
73 {kind: "onyx.Toolbar", classes: "onyx-menu-toolbar", components: [
74 {kind: "onyx.Button", name: "backButton", content: "_logout".loc(),
76 {kind: "Group", name: "iconButtonGroup", tag: null, components: [
77 {kind: "XV.IconButton", name: "historyIconButton",
78 src: "/assets/menu-icon-bookmark.png",
79 ontap: "showHistory", content: "_history".loc()},
80 {kind: "XV.IconButton", name: "searchIconButton",
81 src: "/assets/menu-icon-search.png",
82 ontap: "showParameters", content: "_advancedSearch".loc(), showing: false}
84 {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "actionSelected", components: [
85 {kind: "XV.IconButton", src: "/assets/menu-icon-gear.png",
86 content: "_actions".loc(), name: "actionButton",
87 classes: "xv-action-icon"},
88 {kind: "onyx.Menu", name: "actionMenu"}
90 {kind: "onyx.Popup", name: "aboutPopup", centered: true,
91 modal: true, floating: true, scrim: true, components: [
92 {content: "Copyright xTuple 2014" },
93 {name: "aboutVersion", allowHtml: true },
94 {kind: "onyx.Button", content: "_ok".loc(), ontap: "closeAboutPopup"}
97 {name: "loginInfo", content: "", classes: "xv-navigator-header"},
98 {name: "menuPanels", kind: "Panels", draggable: false, fit: true,
99 margin: 0, components: [
100 {name: "moduleMenu", kind: "List", touch: true,
101 onSetupItem: "setupModuleMenuItem", ontap: "menuTap",
103 {name: "moduleItem", classes: "item enyo-border-box"}
105 {name: "panelMenu", kind: "List", touch: true,
106 onSetupItem: "setupPanelMenuItem", ontap: "panelTap", components: [
107 {name: "listItem", classes: "item enyo-border-box"}
109 {} // Why do panels only work when there are 3+ objects?
112 {kind: "FittableRows", name: "list-view", components: [
113 // the onyx-menu-toolbar class keeps the popups from being hidden
114 {kind: "onyx.MoreToolbar", name: "contentToolbar",
115 classes: "onyx-menu-toolbar", movedClass: "xv-toolbar-moved", components: [
116 {name: "rightLabel", classes: "xv-toolbar-label"},
117 // The MoreToolbar is a FittableColumnsLayout, so this spacer takes up all available space
118 {name: "spacer", content: "", fit: true},
119 // Selectable "New" menu which is hidden by default
120 {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "newRecord", components: [
121 {kind: "XV.IconButton", src: "/assets/menu-icon-new.png",
122 content: "_new".loc(), name: "newMenuButton", showing: false},
123 {kind: "onyx.Menu", name: "newMenu"}
125 // Button to initiate new workspace
126 {kind: "XV.IconButton", src: "/assets/menu-icon-new.png",
127 content: "_new".loc(), name: "newButton", ontap: "newRecord", showing: false},
128 {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "exportSelected", components: [
129 {kind: "XV.IconButton", src: "/assets/menu-icon-export.png",
130 content: "_export".loc(), name: "exportButton"},
131 {kind: "onyx.Menu", name: "exportMenu", showing: false}
133 {kind: "XV.IconButton", name: "sortButton", content: "_sort".loc(),
134 src: "/assets/sort-icon.png", ontap: "showSortPopup", showing: false,
135 classes: "xv-sort-icon"},
136 {name: "refreshButton", kind: "XV.IconButton",
137 src: "/assets/menu-icon-refresh.png", content: "_refresh".loc(),
138 ontap: "requery", showing: false},
139 {name: "search", kind: "onyx.InputDecorator",
140 showing: false, components: [
141 {name: 'searchInput', kind: "onyx.Input", style: "width: 200px;",
142 placeholder: "_search".loc(), onchange: "inputChanged"},
143 {kind: "Image", src: "/assets/search-input-search.png",
144 name: "searchJump", ontap: "jump"},
145 {kind: "XV.SortPopup", name: "sortPopup", showing: false}
148 {name: "messageHeader", content: "", classes: ""},
149 {name: "header", content: "", classes: ""},
150 {name: "contentPanels", kind: "Panels", margin: 0, fit: true,
151 draggable: false, panelCount: 0, classes: "scroll-ios"},
152 {name: "myAccountPopup", kind: "XV.MyAccountPopup"},
153 {name: "listItemMenu", kind: "onyx.Menu", floating: true, onSelect: "listActionSelected",
154 maxHeight: 500, components: []
159 Keeps track of whether any list has already been fetched, to avoid unnecessary
163 actionSelected: function (inSender, inEvent) {
164 var index = this.$.menuPanels.getIndex(),
166 action = inEvent.originator.action,
167 method = action.method || action.name;
169 // Determine if action is coming from a more specific context
170 if (action.context) {
171 context = this.$.contentPanels.getActive();
172 } else if (index && !action.alwaysShowing) {
173 context = this.getSelectedModule();
175 context[method](inSender, inEvent);
177 activate: function () {
178 this.setMenuPanel(MODULE_MENU);
181 The back button is a logout button if you're at the root menu. Otherwise it's a
182 back button that takes you to the root menu.
184 backTapped: function () {
185 var index = this.$.menuPanels.getIndex();
186 if (index === MODULE_MENU) {
189 type: XM.Model.QUESTION,
190 callback: function (response) {
191 if (response.answer) {
195 message: "_logoutConfirmation".loc()
197 this.doNotify(inEvent);
199 this.setHeaderContent("");
200 this.setMenuPanel(MODULE_MENU);
204 If we're on the main menu, then use the navigator actions defined
205 in its `actions` property. If we're in a module, then use the module's
208 buildMenus: function () {
209 var actionMenu = this.$.actionMenu,
210 exportMenu = this.$.exportMenu,
211 newMenu = this.$.newMenu,
212 // This is the index of the active panel. All panels
213 // that have been selected from the menus have an index of
214 // greater than 0. The welcome page has an index of 0.
215 idx = this.$.menuPanels.getIndex(),
216 activePanel = this.$.contentPanels.getActive(),
217 ary = idx ? (this.getSelectedModule().actions || []).concat(this.getActions(true)) : this.getActions(),
218 actions = ary.slice(0),
219 privileges = XT.session.privileges;
223 actionMenu.destroyClientControls();
225 if (idx && activePanel.getNavigatorActions) {
226 _.each(activePanel.getNavigatorActions(), function (action) {
227 action.context = activePanel;
228 actions.push(action);
232 // then add whatever actions are applicable to the current context
233 _.each(actions, function (action) {
234 var name = action.name,
235 privilege = action.privilege,
236 isDisabled = privilege ? privileges.get(privilege) : false;
237 actionMenu.createComponent({
240 content: action.label || ("_" + name).loc(),
247 this.$.actionButton.setShowing(actions.length);
253 exportMenu.destroyClientControls();
255 if (idx && activePanel.getExportActions) {
256 _.each(activePanel.getExportActions(), function (action) {
257 action.context = activePanel;
258 actions.push(action);
262 // then add whatever actions are applicable to the current context
263 _.each(actions, function (action) {
264 var name = action.name,
265 privilege = action.privilege,
266 isDisabled = privilege ? privileges.get(privilege) : false;
267 exportMenu.createComponent({
270 content: action.label || ("_" + name).loc(),
276 this.$.exportButton.setShowing(actions.length);
278 // HANDLE SORT BUTTON
279 // if the activepanel is a list of some kind, show the button
280 if (activePanel.kindClasses) {
281 this.$.sortButton.setShowing(activePanel.kindClasses.indexOf("list") !== -1);
283 this.$.sortButton.setShowing(false);
290 newMenu.destroyClientControls();
292 // XXX not sure about possible falsy condition of idx here
293 if ((idx || idx === 0) && activePanel.getNewActions) {
294 _.each(activePanel.getNewActions(), function (action) {
295 action.context = activePanel;
296 actions.push(action);
300 // then add whatever actions are applicable to the current context
301 _.each(actions, function (action) {
302 newMenu.createComponent({
305 content: action.label || ("_" + action.name).loc(),
306 // this item is the payload from the menu item
307 // that can be handled differently depending on the list.
309 defaults: action.defaults,
310 allowNew: action.allowNew
316 If there is a parameter widget, send the current list to the
317 layout form to build the list of columns.
319 buildLayout: function () {
320 var list = this.$.contentPanels.getActive(),
321 parameterWidget = XT.app ? XT.app.$.pullout.getParameterWidget(list.name) : null;
322 if (parameterWidget && parameterWidget.showLayout) {
323 parameterWidget.buildColumnList(list);
327 The navigator only keeps three panels in the DOM at a time. Anything extra panels
328 will be periodically cached into the panelCache published field and removed from the DOM.
330 cachePanels: function () {
331 var contentPanels = this.$.contentPanels,
336 findPanel = function (panel) {
337 return panel.index === globalIndex;
339 findModule = function (module) {
340 var panel = _.find(module.panels, findPanel);
341 return panel !== undefined;
344 while (contentPanels.children.length > 3) {
345 panelToCache = contentPanels.children[0];
346 globalIndex = panelToCache.index;
348 // Panels are abstractly referenced in this.getModules().
349 // Find the abstract panel of the panelToCache
350 // XXX this would be cleaner if we kept a backwards reference
351 // from the panel to its containing module (and index therein)
352 pertinentModule = _.find(this.getModules(), findModule);
353 panelReference = _.find(pertinentModule.panels, findPanel);
355 contentPanels.removeChild(panelToCache);
356 // only render the most recent (i.e. active) child
357 contentPanels.children[2].render();
358 panelReference.status = "cached";
359 this.getPanelCache()[globalIndex] = panelToCache;
363 When a new column value is selected in the layout panel,
364 this value replaces the old attribute value in the list
367 changeLayout: function (inSender, inEvent) {
368 var newValue = inEvent.value ? inEvent.value : "",
369 order = inEvent.order,
370 list = this.$.contentPanels.getActive(),
371 // get the current list of attribute kinds
372 currentColumns = _.filter(list.$, function (item) {
373 return item.kind === "XV.ListAttr";
376 // When we get to the current selected list attribute,
377 // set the new value in place of the old attribute
378 for (var i = 0; i < currentColumns.length; i++) {
379 var col = currentColumns[i];
380 if (order === (i + 1)) {
381 col.setAttr(newValue);
385 // requery this list with the new attribute values
388 // rebuild the tree of columns
391 return true; // stop right here
393 clearMessage: function () {
394 this.$.messageHeader.setContent("");
395 this.$.messageHeader.setClasses("");
397 closeAboutPopup: function () {
398 this.$.aboutPopup.hide();
400 create: function () {
401 this.inherited(arguments);
403 callback = function () {
407 // If not everything is loaded yet, come back to it later
408 if (!XT.session || !XT.session.privileges) {
409 XT.getStartupManager().registerCallback(callback);
414 exportSelected: function (inSender, inEvent) {
415 var index = this.$.menuPanels.getIndex(),
417 action = inEvent.originator.action,
418 method = action.method || action.name;
420 action.context[method](inSender, inEvent);
422 getActions: function (alwaysShowingOnly) {
423 var actions = this.actions;
424 if (alwaysShowingOnly) {
425 actions = _.filter(actions, function (action) {
426 return action.alwaysShowing === true;
431 getSelectedModule: function () {
432 return this._selectedModule;
435 Exports the contents of a list to CSV. Note that it will export the entire
436 list, not just the part that's been lazy-loaded. Of course, it will apply
437 the filter criteria as selected. Goes to the server for this.
438 Avoids websockets or AJAX because the server will prompt the browser to download
439 the file by setting the Content-Type of the response, which is not possible with
442 exportList: function (inSender, inEvent) {
443 this.openExportTab('export');
446 newTab: function () {
447 window.open(XT.getOrganizationPath() + '/app', '_newtab');
449 openPreferencesWorkspace: function () {
450 this.doWorkspace({workspace: "XV.UserPreferenceWorkspace", id: false});
452 reportList: function (inSender, inEvent) {
453 this.openExportTab('report');
456 printList: function (inSender, inEvent) {
457 var list = this.$.contentPanels.getActive(),
458 recordType = list.value.model.prototype.recordType,
459 query = JSON.parse(JSON.stringify(list.getQuery())); // clone
461 XT.DataSource.callRoute(
462 'report?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@,"print":%@}'
463 .f(recordType.prefix(), recordType.suffix(), JSON.stringify(query), JSON.stringify(XT.locale.culture), "true"),
465 {success: function (result) {
467 // Here's where we could do a popup for print status
469 if (XT.session.config.debugging) {
470 XT.log("Print route success, type: ", recordType.suffix());
473 error: function (result) {
474 if (XT.session.config.debugging) {
475 XT.log("Print route error, type: ", recordType.suffix());
484 printSelectList: function (inSender, inEvent) {
485 var list = this.$.contentPanels.getActive(),
486 selected = list.getSelection().getSelected();
488 _.each(selected, function (value, index) {
489 var model = list.getModel(index),
490 modelName = model.editableModel || model.recordType,
491 reportName = modelName.suffix(),
493 nameSpace: modelName.prefix(),
494 type: modelName.suffix(),
497 culture: XT.locale.culture,
501 XT.DataSource.callRoute(
502 "report?details=%@".f(JSON.stringify(details)),
504 {success: function (result) {
506 // Here's where we could do a popup for print status
508 if (XT.session.config.debugging) {
509 XT.log("Print route success, type: ", modelName.suffix());
512 error: function (result) {
513 if (XT.session.config.debugging) {
514 XT.log("Print route error, type: ", modelName.suffix());
526 * If a reportSelectList is needed, here it is. This will open a browser
527 * tab for the first report and new windows for the other reports.
529 reportSelectList: function (inSender, inEvent) {
530 var list = this.$.contentPanels.getActive(),
531 selected = list.getSelection().getSelected();
533 _.each(selected, function (value, index) {
534 var model = list.getModel(index),
535 modelName = model.editableModel || model.recordType,
536 reportName = modelName.suffix(),
538 nameSpace: modelName.prefix(),
539 type: modelName.suffix(),
542 culture: XT.locale.culture
544 // sending the locale information back over the wire saves a call to the db
545 console.log("printing id: " + model.id);
546 window.open(XT.getOrganizationPath() +
547 "/report?details=%@".f(JSON.stringify(details)), "Print: " + model.id);
553 openExportTab: function (routeName) {
554 var list = this.$.contentPanels.getActive(),
555 recordType = list.value.model.prototype.recordType,
556 query = JSON.parse(JSON.stringify(list.getQuery())); // clone
558 delete query.rowLimit;
559 delete query.rowOffset;
561 // sending the locale information back over the wire saves a call to the db
562 window.open(XT.getOrganizationPath() +
563 '/%@?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@,"print":%@}'
567 JSON.stringify(query),
568 JSON.stringify(XT.locale.culture),
572 showSortPopup: function (inSender, inEvent) {
573 this.$.sortPopup.setList(this.$.contentPanels.getActive());
574 this.$.sortPopup.setNav(this);
575 this.$.sortPopup.setPickerStrings();
576 this.$.sortPopup.show();
582 fetch: function (options) {
583 options = options ? _.clone(options) : {};
584 var list = this.$.contentPanels.getActive(),
585 name = list ? list.name : "",
592 // in order to continue to the fetch, this needs to be a list
595 if (!(list instanceof XV.List) && !(list instanceof XV.Dashboard) && !(list instanceof XV.Listboard)) { return; }
597 query = list.getQuery() || {};
598 input = this.$.searchInput.getValue();
600 // if the "list" doesn't allow dynamic searching, skip this
601 // Dashboards have an allowFilter of false
602 if (list.allowFilter) {
603 parameterWidget = XT.app ? XT.app.$.pullout.getParameterWidget(name) : null;
604 parameters = parameterWidget ? parameterWidget.getParameters() : [];
605 options.showMore = _.isBoolean(options.showMore) ?
606 options.showMore : false;
608 // Get information from filters and set description
609 filterDescription = this.formatQuery(parameterWidget ? parameterWidget.getSelectedValues() : null, input);
610 list.setFilterDescription(filterDescription);
611 this.setHeaderContent(filterDescription);
613 delete query.parameters;
615 if (input || parameters.length) {
616 query.parameters = [];
618 // Input search parameters
620 query.parameters.push({
621 attribute: list.getSearchableAttributes(),
623 value: this.$.searchInput.getValue()
627 // Advanced parameters
629 query.parameters = query.parameters.concat(parameters);
633 // if there is a parameter widget for this list, build the columns
634 if (parameterWidget && parameterWidget.showLayout) {
639 list.setQuery(query);
642 formatQuery: function (advancedSearch, simpleSearch) {
646 for (key in advancedSearch) {
647 formattedQuery += (key + ": " + advancedSearch[key] + ", ");
650 if (simpleSearch && formattedQuery) {
651 formattedQuery += "_match".loc() + ": " + simpleSearch;
652 } else if (simpleSearch) {
653 formattedQuery += simpleSearch;
656 if (formattedQuery) {
657 formattedQuery = "_filterBy".loc() + ": " + formattedQuery;
660 if (formattedQuery.lastIndexOf(", ") + 2 === formattedQuery.length) {
661 // chop off trailing comma
662 formattedQuery = formattedQuery.substring(0, formattedQuery.length - 2);
664 return formattedQuery;
666 handleHotKey: function (inSender, inEvent) {
667 var destinationIndex,
668 isWelcome = this.getSelectedModule().name === 'welcome',
670 keyCode = inEvent.keyCode;
672 // numbers navigate to the nth menu option
673 if (keyCode >= 49 && keyCode <= 57) {
674 destinationIndex = keyCode - 49;
676 this.setModule(Math.min(destinationIndex, this.getModules().length - 1));
678 this.setContentPanel(Math.min(destinationIndex, this.getSelectedModule().panels.length - 1));
682 } else if (!isWelcome) {
683 currentIndex = this.$.panelMenu.getSelection().lastSelected;
684 if (keyCode === 38) {
685 this.setContentPanel(Math.max(currentIndex - 1, 0));
687 } else if (keyCode === 40) {
688 this.setContentPanel(Math.min(currentIndex + 1, this.getSelectedModule().panels.length - 1));
693 switch(String.fromCharCode(keyCode)) {
695 this.showParameters();
704 this.newRecord({}, {originator: {}});
711 inputChanged: function (inSender, inEvent) {
716 Drills down into a workspace if a user clicks a list item.
718 itemTap: function (inSender, inEvent) {
719 var workspace = inEvent.originator.getWorkspace(),
720 model = inEvent.model,
721 canNotRead = model.couldRead ? !model.couldRead() : !model.getClass().canRead(),
722 id = model && model.id ? model.id : false;
724 // Check privileges first
726 this.showError("_insufficientViewPrivileges".loc());
730 // Bubble requset for workspace view, including the model id payload
732 this.doWorkspace({workspace: workspace, id: id});
737 var list = this.$.contentPanels.getActive(),
738 workspace = list ? list.getWorkspace() : null,
739 Klass = list.getValue().model,
740 upper = this._getModelProperty(Klass, 'enforceUpperKey'),
741 input = this.$.searchInput.getValue(),
744 key = this._getModelProperty(Klass, 'documentKey'),
747 if (this._busy || !input || !key) { return; }
750 // First find a matching id
751 options.success = function (id) {
755 // Next fetch the model, see if we have privs
756 options.success = function () {
757 var canNotRead = model.couldRead ?
758 !model.couldRead() : !model.getClass().canRead();
760 // Check privileges first
762 this.showError("_insufficientViewPrivileges".loc());
765 // Bubble requset for workspace view, including the model id payload
766 if (workspace) { that.doWorkspace({workspace: workspace, id: id}); }
770 attrs[Klass.prototype.idAttribute] = id;
771 model = Klass.findOrCreate(attrs);
772 model.fetch(options);
774 that.showError("_noDocumentFound".loc());
775 that.$.searchInput.clear();
779 input = upper ? input.toUpperCase() : input;
780 Klass.findExisting(key, input, options);
782 loginInfo: function () {
783 return this.$.loginInfo;
786 Handles additive changes only
788 modulesChanged: function () {
789 var modules = this.getModules() || [],
790 existingModules = this._modules || [],
797 findExistingModule = function (name) {
798 return _.find(existingModules, function (module) {
799 return module.name === name;
802 findExistingPanel = function (panels, name) {
803 return _.find(panels, function (panel) {
804 return panel.name === name;
809 for (i = 0; i < modules.length; i++) {
810 panels = modules[i].panels || [];
811 existingModule = findExistingModule(modules[i].name);
812 for (n = 0; n < panels.length; n++) {
814 // If the panel already exists, move on
815 if (existingModule) {
816 existingPanel = findExistingPanel(existingModule.panels, panels[n].name);
817 if (existingPanel) { continue; }
820 // Keep track of where this panel is being placed for later reference
821 panels[n].index = this.$.contentPanels.panelCount++;
823 // XXX try this: only create the first three
824 if (panels[n].index < 3) {
825 panels[n].status = "active";
827 // Default behavior for Lists is toggle selections
828 // So we can perform actions on rows. If not a List
829 // this property shouldn't hurt anything
830 if (panels[n].toggleSelected === undefined) {
831 panels[n].toggleSelected = true;
833 panel = this.$.contentPanels.createComponent(panels[n]);
834 if (panel instanceof XV.List) {
836 // Bubble parameter widget up to pullout
837 this.doListAdded(panel);
840 panels[n].status = "unborn";
844 this.$.moduleMenu.setCount(modules.length);
846 this._modules = JSON.parse(JSON.stringify(modules));
850 Fired when the user clicks the "New" button. Takes the user to a workspace
851 backed by an empty object of the type displayed in the current list.
853 newRecord: function (inSender, inEvent) {
854 var list = this.$.contentPanels.getActive(),
855 workspace = list instanceof XV.List ? list.getWorkspace() : null,
856 item = inEvent.originator.item,
857 defaults = inEvent.originator.defaults,
858 allowNew = inEvent.originator.allowNew,
863 // This list is actually a dashboard so the item
864 // will be a type of chart added to the dashboard
865 if (list instanceof XV.Dashboard && item) {
866 list.newRecord(item);
870 if (list instanceof XV.Listboard && item) {
871 list.newRecord(item);
875 if (!list instanceof XV.List) {
880 Model = list.getValue().model;
881 canCreate = Model.couldCreate ? Model.couldCreate() : Model.canCreate();
883 this.showError("_insufficientCreatePrivileges".loc());
890 workspace: workspace,
891 attributes: defaults,
892 allowNew: allowNew === false ? false : true
896 // In addition to preventing Enyo event propagation,
897 // we need to prevent propagation of DOM events to support
898 // mobile browsers and long button clicks
899 // check to make sure this is a button click before calling inEvent function
900 if (inEvent && inEvent.preventDefault) {
901 inEvent.preventDefault();
905 popupHidden: function (inSender, inEvent) {
906 if (!this._popupDone) {
907 inEvent.originator.show();
910 requery: function (inSender, inEvent) {
914 Determines whether the advanced search or the history icon (or neither) is
917 setActiveIconButton: function (buttonName) {
918 var activeIconButton = null;
919 // Null deactivates both
920 if (buttonName === 'search') {
921 activeIconButton = this.$.searchIconButton;
922 } else if (buttonName === 'history') {
923 activeIconButton = this.$.historyIconButton;
925 this.$.iconButtonGroup.setActive(activeIconButton);
928 Renders a list and performs all the necessary auxilliary work such as hiding/showing
929 the advanced search icon if appropriate. Called when a user chooses a menu item.
931 setContentPanel: function (index) {
932 var contentPanels = this.$.contentPanels,
933 module = this.getSelectedModule(),
934 panelIndex = module && module.panels ? module.panels[index].index : -1,
935 panelStatus = module && module.panels ? module.panels[index].status : 'unknown',
943 if (panelStatus === 'active') {
944 panel = _.find(contentPanels.children, function (child) {
945 return child.index === panelIndex;
947 } else if (panelStatus === 'unborn') {
948 // panel exists but has not been rendered. Render it.
949 module.panels[index].status = 'active';
951 // Default behavior for Lists is toggle selections
952 // So we can perform actions on rows. If not a List
953 // this property shouldn't hurt anything
954 if (module.panels[index].toggleSelected === undefined) {
955 module.panels[index].toggleSelected = true;
957 panel = contentPanels.createComponent(module.panels[index]);
959 if (panel instanceof XV.List) {
961 // Bubble parameter widget up to pullout
962 this.doListAdded(panel);
965 } else if (panelStatus === 'cached') {
966 module.panels[index].status = 'active';
967 panel = this.panelCache[panelIndex];
968 contentPanels.addChild(panel);
969 panel.node = undefined; // new to enyo2.2! wipe out the node so that it can get re-rendered fresh
973 XT.error("Don't know what to do with this panel status");
976 // If we're already here, bail
977 if (contentPanels.index === this.$.contentPanels.indexOfChild(panel)) {
981 // cache any extraneous content panels
984 label = panel && panel.label ? panel.label : "";
985 collection = panel && panel.getCollection ? XT.getObjectByName(panel.getCollection()) : false;
987 if (!panel) { return; }
989 // Make sure the advanced search icon is visible iff there is an advanced
990 // search for this list
991 if (panel.parameterWidget) {
992 this.$.searchIconButton.setShowing(true);
994 this.$.searchIconButton.setShowing(false);
996 this.doNavigatorEvent({name: panel.name, show: false});
999 this.$.newButton.setShowing(panel.canAddNew && !panel.newActions);
1000 this.$.newMenuButton.setShowing(panel.canAddNew && panel.newActions);
1002 if (panel.canAddNew && collection) {
1003 // Check 'couldCreate' first in case it's an info model.
1004 model = collection.prototype.model;
1005 canNotCreate = model.prototype.couldCreate ? !model.prototype.couldCreate() : !model.canCreate();
1007 this.$.newButton.setDisabled(canNotCreate);
1010 if (!this.$.panelMenu.isSelected(index)) {
1011 this.$.panelMenu.select(index);
1015 contentPanels.setIndex(this.$.contentPanels.indexOfChild(panel));
1017 this.$.rightLabel.setContent(label);
1018 if (panel.getFilterDescription) {
1019 this.setHeaderContent(panel.getFilterDescription());
1021 if (panel.fetch && !this.fetched[panelIndex]) {
1023 this.fetched[panelIndex] = true;
1027 this.$.contentToolbar.resized();
1031 The header content typically describes to the user the particular query filter in effect.
1033 setHeaderContent: function (content) {
1034 this.$.header.setContent(content);
1035 if (content !== "") {
1036 this.$.header.setClasses("xv-navigator-header");
1038 this.$.header.setClasses("");
1041 setMenuPanel: function (index) {
1042 var label = index ? "_back".loc() : "_logout".loc();
1043 this.$.menuPanels.setIndex(index);
1044 this.$.menuPanels.getActive().select(0);
1045 this.setContentPanel(0);
1046 // this causes the gear menu to reload when the module
1047 // is chosen, rather than just when a content panel is selected
1050 this.$.backButton.setContent(label);
1051 this.$.refreshButton.setShowing(index);
1052 this.$.search.setShowing(index);
1053 this.$.contentToolbar.resized();
1055 setMessageContent: function (inSender, inEvent) {
1056 var content = inEvent.message;
1058 this.$.messageHeader.setContent(content);
1059 if (content !== "") {
1060 this.$.messageHeader.setClasses("xv-navigator-header");
1062 this.$.messageHeader.setClasses("");
1065 setModule: function (index) {
1066 var module = this.getModules()[index],
1067 panels = module.panels || [],
1068 hasSubmenu = module.hasSubmenu !== false && panels.length;
1069 if (module !== this._selectedModule || enyo.Panels.isScreenNarrow()) {
1070 this._selectedModule = module;
1072 this.$.panelMenu.setCount(panels.length);
1073 this.$.panelMenu.render();
1074 this.setMenuPanel(PANEL_MENU);
1076 // if no submenus, treat lke a panel
1077 this.setContentPanel(0);
1081 setModules: function (modules) {
1082 this.modules = modules;
1083 this.modulesChanged();
1086 Renders a list of modules from the root menu.
1088 setupModuleMenuItem: function (inSender, inEvent) {
1089 var index = inEvent.index,
1090 label = this.modules[index].label,
1091 isSelected = inSender.isSelected(index);
1092 this.$.moduleItem.setContent(label);
1093 this.$.moduleItem.addRemoveClass("onyx-selected", isSelected);
1094 if (isSelected) { this.setModule(index); }
1097 Renders the leftbar list of objects within a given module. This function
1098 is also called when a leftbar item is tapped, per enyo's List conventions.
1100 setupPanelMenuItem: function (inSender, inEvent) {
1101 var module = this.getSelectedModule(),
1102 index = inEvent.index,
1103 isSelected = inSender.isSelected(index),
1104 panel = module.panels[index],
1105 name = panel && panel.name ? module.panels[index].name : "",
1106 // peek inside the kind to see what the label should be
1107 kind = panel && panel.kind ? XT.getObjectByName(panel.kind) : null,
1108 label = kind && kind.prototype.label ? kind.prototype.label : "",
1111 if (!label && kind && kind.prototype.determineLabel) {
1112 // some of these lists have labels that are dynamically computed,
1113 // so we can't rely on their being statically defined. We have to
1114 // compute them in the same way that their create() method would.
1115 shortKindName = panel.kind.substring(0, panel.kind.length - 4).substring(3);
1116 label = kind.prototype.determineLabel(shortKindName);
1118 } else if (!label) {
1119 label = panel ? panel.label || name : name;
1122 this.$.listItem.setContent(label);
1123 this.$.listItem.addRemoveClass("onyx-selected", isSelected);
1127 This function is called when a panel is selected. If the selection is valid,
1128 then the content panel is set for that panel selection.
1130 panelTap: function (inSender, inEvent) {
1131 var index = inEvent.index, validIndex = index || index === 0;
1132 if (validIndex && inSender.isSelected(index)) { // make sure an item in the list was clicked and is selected
1133 this.setContentPanel(index);
1138 This function is called when a module is selected. If the selection is valid,
1139 then the list of panels is shown for that module.
1141 menuTap: function (inSender, inEvent) {
1142 var validIndex = inEvent.index || inEvent.index === 0;
1143 if (validIndex) { // make sure an item in the list was clicked
1144 this.setupModuleMenuItem(inSender, inEvent);
1147 showAbout: function () {
1148 this.$.aboutPopup.show();
1151 Error notification, using XV.ModuleContainer notify mechanism
1153 showError: function (message) {
1156 type: XM.Model.CRITICAL,
1159 this.doNotify(inEvent);
1161 showHelp: function () {
1169 listName = this.$.contentPanels.getActive().name;
1170 culture = XT.locale.culture;
1171 objectName = (listName.indexOf("List") >= 0 || listName.indexOf("Page") >= 0) ?
1172 listName.substring(0, listName.length - 4) : // get rid of the word "List" or "Page"
1174 objectName = objectName.indexOf("_") >= 0 ?
1175 objectName.substring(1 + objectName.indexOf("_")) : // strip out underscore-terminated-prefixes
1177 pageName = objectName.decamelize().replace(/_/g, "-");
1178 url = XT.HELP_URL_ROOT + pageName + "?culture=" + culture;
1179 panel = {name: 'help', show: true, url: url};
1181 this.doNavigatorEvent(panel);
1184 Displays the history panel.
1186 showHistory: function (inSender, inEvent) {
1187 var panel = {name: 'history', show: true};
1188 this.doNavigatorEvent(panel);
1191 Displays the advanced search panel.
1193 showParameters: function (inSender, inEvent) {
1194 var list = this.$.contentPanels.getActive();
1195 this.doNavigatorEvent({name: list.name, show: true});
1198 Displays the My Account popup.
1200 showMyAccount: function (inSender, inEvent) {
1201 this.$.myAccountPopup.show();
1204 _getModelProperty: function (Klass, prop) {
1206 // Get the key if it's a document model
1207 if (Klass.prototype[prop]) {
1208 ret = Klass.prototype[prop];
1210 // Hopefully it's an info model
1211 } else if (Klass.prototype.editableModel) {
1212 Klass = XT.getObjectByName(Klass.prototype.editableModel);
1213 ret = Klass.prototype[prop];
1219 enyo.mixin(navigator, XV.ListMenuManagerMixin);
1220 enyo.kind(navigator);