address button comment and jshint errors
[xtuple] / lib / enyo-x / source / views / workspace.js
1 /*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true,
2 latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
3 trailing:true, white:true, strict: false*/
4 /*global XV:true, XM:true, _:true, enyo:true, XT:true, Globalize:true, window:true */
5
6 (function () {
7   var SAVE_APPLY = 1;
8   var SAVE_CLOSE = 2;
9   var SAVE_NEW = 3;
10
11   /**
12     @name XV.EditorMixin
13     @class A mixin that contains functionality common to {@link XV.Workspace}
14      and {@link XV.ListRelationsEditorBox}.
15    */
16  /**
17     @name XV.EditorMixin
18     @class A mixin that contains functionality common to {@link XV.Workspace}
19      and {@link XV.ListRelationsEditorBox}.
20    */
21   XV.EditorMixin = {
22     controlValueChanged: function (inSender, inEvent) {
23       var attrs = {},
24         attr = inEvent.originator.attr;
25
26       if (this.value && attr) {
27         // If the value is an object, then the object is already mapped
28         if (attr instanceof Object) {
29           attrs = inEvent.value;
30
31         // Otherwise map a basic value to its attribute
32         } else {
33           attrs[attr] = inEvent.value;
34         }
35         this.value.setValue(attrs);
36         return true;
37       } else if (XT.session.config.debugging) {
38         XT.log("Ignoring content-less model update event", this.value, attr);
39       }
40     },
41     /**
42       Updates all child controls on the editor where the name of
43       the control matches the name of an attribute on the model.
44
45       @param {XM.Model} model
46       @param {Object} options
47     */
48     attributesChanged: function (model, options) {
49       // If the model is meta, then move up to the workspace model
50       if (!(model instanceof XM.Model)) { model = this.value; }
51
52       options = options || {};
53       var value,
54         K = XM.Model,
55         status = model.getStatus(),
56         canView = true,
57         canUpdate = (status === K.READY_NEW) ||
58           ((status & K.READY) && model.canUpdate()),
59         isReadOnly = false,
60         isRequired,
61         prop,
62         modelPropName,
63         attribute,
64         obj,
65         ctrls = this.$;
66
67       // loop through each of the controls and set the value using the
68       // attribute on the control
69       _.each(ctrls, function (control) {
70         if (control.attr) {
71           if (!control.getAttr) {
72             XT.log("Warning: " + control.kind + " is not a valid XV control");
73           }
74           attribute = control.getAttr();
75
76           // for compound controls, the attribute is an object with key/value pairs.
77           if (_.isObject(attribute)) {
78             obj = _.clone(attribute);
79             // replace the current values in the object (attribute names)
80             // with the values from the model
81             for (var attr in obj) {
82               if (obj.hasOwnProperty(attr)) {
83                 prop = model.attributeDelegates && model.attributeDelegates[attr] ?
84                   model.attributeDelegates[attr] : attr;
85                 // replace the current value of the property name with the value from the model
86                 modelPropName = obj[prop];
87                 obj[prop] = model.getValue(modelPropName);
88               }
89             }
90             value = obj;
91
92             // only worry about the one mapping to isEditableProperty
93             if (control.isEditableProperty) {
94               attribute = attribute[control.isEditableProperty];
95
96             // If not isEditableProperty defined one is as good as another, just pick one
97             } else {
98               attribute = attribute[_.first(_.keys(attribute))];
99             }
100           }
101
102           prop = model.attributeDelegates && model.attributeDelegates[attribute] ?
103             model.attributeDelegates[attribute] : attribute;
104
105           if (!obj) { value = model.getValue(prop); }
106
107           canView = model.canView(prop);
108           isReadOnly = model.isReadOnly(prop) || !model.canEdit(prop);
109           isRequired = model.isRequired(prop);
110
111           if (canView) {
112             control.setShowing(true);
113             if (control.setPlaceholder && isRequired && !control.getPlaceholder()) {
114               control.setPlaceholder("_required".loc());
115             }
116             if (control.setValue && !(status & K.BUSY)) {
117               control.setValue(value, {silent: true});
118             }
119             if (control.setDisabled) {
120               control.setDisabled(!canUpdate || isReadOnly);
121             }
122           } else {
123             control.setShowing(false);
124           }
125           isReadOnly = false;
126           canView = true;
127           obj = undefined;
128         }
129       }, this);
130     },
131     /**
132      @todo Document the clear method.
133      */
134     clear: function () {
135       var attrs = this.value ? this.value.getAttributeNames() : [],
136         attr,
137         control,
138         i;
139       for (i = 0; i < attrs.length; i++) {
140         attr = attrs[i];
141         control = this.findControl(attr);
142         if (control && control.clear) {
143           control.clear({silent: true});
144         }
145       }
146     },
147     /**
148      Returns the control that contains the attribute string.
149      */
150     findControl: function (attr) {
151       return _.find(this.$, function (ctl) {
152         if (_.isObject(ctl.attr)) {
153           return _.find(ctl.attr, function (str) {
154             return str === attr;
155           });
156         }
157         return ctl.attr === attr;
158       });
159     },
160     /**
161       Bubble up an event to ask a question to the user. The user interaction
162       is handled by XV.ModuleContainer.
163      */
164     notify: function (model, message, options) {
165       var inEvent = _.extend(options, {
166         originator: this,
167         model: model,
168         message: message
169       });
170       this.doNotify(inEvent);
171     }
172   };
173
174   /**
175     Set model bindings on a workspace
176     @private
177     @param{Object} Workspace
178     @param{String} Action: 'on' or 'off'
179   */
180   var _setBindings = function (ws, action) {
181     var headerAttrs = ws.getHeaderAttrs() || [],
182       observers = "",
183       model = ws.value,
184       attr,
185       i;
186
187     model[action]("change", ws.attributesChanged, ws);
188     model[action]("lockChange", ws.lockChanged, ws);
189     model[action]("readOnlyChange", ws.attributesChanged, ws);
190     model[action]("statusChange", ws.statusChanged, ws);
191     model[action]("invalid", ws.error, ws);
192     model[action]("error", ws.error, ws);
193     model[action]("notify", ws.notify, ws);
194     if (headerAttrs.length) {
195       for (i = 0; i < headerAttrs.length; i++) {
196         attr = headerAttrs[i];
197         if (attr.indexOf('.') !== -1 ||
198             _.contains(model.getAttributeNames(), attr)) {
199           observers = observers ? observers + " change:" + attr : "change:" + attr;
200         }
201       }
202       model[action](observers, ws.headerValuesChanged, ws);
203     }
204     // If meta data exists, handle that too.
205     if (model.meta) {
206       model.meta[action]("change", ws.attributesChanged, ws);
207     }
208   };
209
210   XV.WorkspacePanelsRefactor = {
211
212     rendered: function () {
213       if (!this.$.panels || !this.$.panels.hasClass('xv-workspace-panel')) {
214         this.inherited(arguments);
215         return;
216       }
217
218       _.each(this.$.panels.getClientControls(), function (control) {
219         control.addClass('xv-workspace-panel');
220       });
221       this.$.panels.removeClass('xv-workspace-panel');
222       this.inherited(arguments);
223     }
224   };
225
226   /**
227     @name XV.Workspace
228     @class Contains a set of fittable rows which are laid out
229     using a collapsing arranger and fitted to the size of the viewport.<br />
230     Its components can be extended via {@link XV.ExtensionsMixin}.<br />
231     Derived from <a href="http://enyojs.com/api/#enyo.FittableRows">enyo.FittableRows</a>.
232
233     Supported properties on the action array are:
234       * name
235       * label: Menu label. Defaults to name if not present.
236       * privilege: The privilege required by the user to enable the menu. Defaults enabled if not present.
237       * prerequisite: A function on the model that returns a boolean dictating whether to show the menu item or not.
238       * method: The function to call. Defaults to name if not present.
239       * isViewMethod: Boolean value dictates whether method is called on the view or the view's model. Default false.
240
241     @extends XV.WorkspacePanels
242     @extends XV.EditorMixin
243     @extends XV.ExtensionsMixin
244     @see XV.WorkspaceContainer
245   */
246   enyo.kind(_.extend({ },
247       XV.EditorMixin, XV.ExtensionsMixin,
248       XV.WorkspacePanelsRefactor, {
249     name: "XV.Workspace",
250     kind: "XV.WorkspacePanels",
251     classes: 'xv-workspace',
252     published: {
253       actions: null,
254       actionButtons: null,
255       title: "_none".loc(),
256       headerAttrs: null,
257       success: null,
258       callback: null,
259       modelAmnesty: false, // do we keep the model even if the workspace is destroyed?
260       // typically no, but yes for child workspaces
261       printOnSaveSetting: "", // some workspaces have a setting that cause them to be
262       // automatically printed upon saving
263       reportName: null,
264       reportModel: null,
265       recordId: null,
266       saveText: "_save".loc(),
267       backText: "_back".loc(),
268       hideSaveAndNew: false,
269       hideApply: false,
270       hideRefresh: false,
271       dirtyWarn: true,
272
273       /**
274        * The type of the backing model.
275        */
276       model: null,
277
278       /**
279        * The backing model for this component.
280        * @see XM.EnyoView#model
281        */
282       value: null,
283
284       /**
285        * @see XM.View#workspace.template
286        */
287       template: null
288     },
289     extensions: null,
290     events: {
291       onClose: "",
292       onError: "",
293       onHeaderChange: "",
294       onModelChange: "",
295       onStatusChange: "",
296       onTitleChange: "",
297       onHistoryChange: "",
298       onLockChange: "",
299       onMenuChange: "",
300       onNotify: "",
301       onSaveTextChange: "",
302       onTransactionList: "",
303       onWorkspace: ""
304     },
305     handlers: {
306       onValueChange: "controlValueChanged"
307     },
308
309     components: [
310       {kind: "XV.Groupbox", name: "mainPanel",
311         components: [
312         {kind: "onyx.GroupboxHeader", content: "_overview".loc()},
313         {kind: "XV.ScrollableGroupbox", name: "mainGroup",
314           classes: "in-panel", fit: true, components: [
315           {kind: "XV.InputWidget", attr: "name"},
316           {kind: "XV.InputWidget", attr: "description"}
317         ]}
318       ]}
319     ],
320
321     create: function () {
322       this.inherited(arguments);
323       XM.View.setPresenter(this, 'workspace');
324       this.processExtensions();
325       this.titleChanged();
326     },
327
328     /**
329      @todo Document the destroy method.
330      */
331     destroy: function () {
332       var model = this.getValue(),
333         modelAmnesty = this.getModelAmnesty(),
334         wasNew = model.isNew();
335       this.setRecordId(null);
336       // If we never saved a new model, make the callback
337       // so the caller can deal with that and destroy it.
338       if (wasNew || modelAmnesty) {
339         if (this.callback) { this.callback(false); }
340       }
341       this.inherited(arguments);
342     },
343     /**
344      @todo Document the error method.
345      */
346     error: function (model, error) {
347       var inEvent = {
348         originator: this,
349         model: model,
350         error: error
351       };
352       this.doError(inEvent);
353       this.attributesChanged(this.getValue());
354     },
355     /**
356      @todo Document the fetch method.
357      */
358     fetch: function (options) {
359       options = options || {};
360       var wsSuccess = this.getSuccess(),
361         success = options.success;
362
363       if (wsSuccess) {
364         wsSuccess = _.bind(wsSuccess, this);
365       }
366
367       options.success = function (model, resp, options) {
368         if (wsSuccess) { wsSuccess(model, resp, options); }
369         if (success) { success(model, resp, options); }
370       };
371       if (!this.value) { return; }
372       this.value.fetch(options);
373     },
374     /**
375       Implementation is up to subkinds
376      */
377     handleHotKey: function (keyValue) {},
378     /**
379      @todo Document the headerValuesChanged method.
380      */
381     headerValuesChanged: function () {
382       var headerAttrs = this.getHeaderAttrs() || [],
383         model = this.value,
384         header = "",
385         value,
386         attr,
387         i;
388       if (headerAttrs.length && model) {
389         for (i = 0; i < headerAttrs.length; i++) {
390           attr = headerAttrs[i];
391           if (attr.indexOf('.') !== -1 ||
392               _.contains(model.getAttributeNames(), attr)) {
393             value = model.getValue(headerAttrs[i]) || "";
394             header = header ? header + " " + value : value;
395           } else {
396             header = header ? header + " " + attr : attr;
397           }
398         }
399       }
400       this.doHeaderChange({originator: this, header: header });
401     },
402     /**
403      @todo Document the isDirty method.
404      */
405     isDirty: function () {
406       return this.value ? this.value.isDirty() : false;
407     },
408     lockChanged: function () {
409       this.doLockChange({hasKey: this.getValue().hasLockKey()});
410     },
411     /**
412      @todo Document the newRecord method.
413      */
414     newRecord: function (attributes, options) {
415       options = options || {};
416       var model = this.getModel(),
417         Klass = XT.getObjectByName(model),
418         attr,
419         changes = {},
420         that = this,
421         relOptions = {
422           success: function () {
423             that.attributesChanged(that.value);
424           }
425         },
426         // Update record id directly when we get it, but don't trigger changes
427         updateRecordId = function (model) {
428           that.recordId = model.id;
429           that.value.off("change:" + that.value.idAttribute, updateRecordId, that);
430         };
431       this.setRecordId(null);
432       this.value = new Klass();
433       _setBindings(this, "on");
434       this.value.on("change:" + this.value.idAttribute, updateRecordId, this);
435       this.clear();
436       this.headerValuesChanged();
437       this.value.initialize(null, {isNew: true});
438       if (options.success) { options.success.call(this); }
439       this.value.set(attributes, {force: true});
440       for (attr in attributes) {
441         if (attributes.hasOwnProperty(attr)) {
442           this.value.setReadOnly(attr);
443           if (this.value.getRelation(attr)) {
444             this.value.fetchRelated(attr, relOptions);
445           } else {
446             changes[attr] = true;
447             this.attributesChanged(this.value, {changes: changes});
448           }
449         }
450       }
451     },
452     // TODO: in 1.8.0 make this the default, move the buttons into a gear
453     // menu, and follow the same option convention as in the list
454     download: function () {
455       this.openReport(XT.getOrganizationPath() + this.getValue().getReportUrl("download"));
456     },
457     /**
458       Email the model's data, either silently or by opening a tab
459      */
460     email: function () {
461       if (XT.session.config.emailAvailable) {
462         // send it to be printed silently by the server
463         this.getValue().doEmail();
464       } else {
465         this.openReport(XT.getOrganizationPath() + this.getValue().getReportUrl());
466       }
467     },
468     /**
469       Print the model's data, either silently or by opening a tab
470     */
471     print: function () {
472       if (XT.session.config.printAvailable) {
473         // send it to be printed silently by the server
474         this.getValue().doPrint();
475       } else {
476         this.openReport(XT.getOrganizationPath() + this.getValue().getReportUrl());
477       }
478     },
479     /**
480       Open the report pdf in a new tab
481      */
482     openReport: function (path) {
483       window.open(path, "_newtab");
484     },
485     /**
486      Handle clearing and reseting of model if the record id changes.
487      */
488     recordIdChanged: function () {
489       var model = this.getModel(),
490         Klass = model ? XT.getObjectByName(model) : null,
491         recordId = this.getRecordId(),
492         attrs = {};
493
494       // Clean up
495       if (this.value) {
496         _setBindings(this, "off");
497         this.value.releaseLock();
498       }
499
500       // the configuration workspaces, notably, need to be fetched despite
501       // having an id of false. It works because the XM.Settings models use
502       // a dispatch for fetch.
503       if (recordId === undefined || recordId === null) {
504         if (this.value.isNew() && !this.modelAmnesty) {
505           this.value.destroy();
506         }
507         this.value = null;
508         return;
509       }
510
511       // Create new instance and bindings
512       if (recordId === false && this.singletonModel) {
513         this.setValue(XT.getObjectByName(this.singletonModel));
514       } else {
515         attrs[Klass.prototype.idAttribute] = recordId;
516         this.setValue(Klass.findOrCreate(attrs));
517       }
518       _setBindings(this, "on");
519       this.fetch();
520     },
521     /**
522       Refresh the model behind this workspace. Note that we
523       have to release the lock first.
524
525       If this is being called on a new model, it means we
526       want to throw away the data in the model and start with
527       a new record. We don't worry about the lock in this case.
528      */
529     requery: function () {
530       if (this.getValue().isNew()) {
531         this.newRecord();
532         return;
533       }
534
535       // model has already been saved
536       var that = this,
537         options = {
538           success: function () {
539             that.fetch();
540           },
541           error: function () {
542             XT.log("Error releasing lock.");
543             // fetch anyway. Why not!?
544             that.fetch();
545           }
546         };
547
548       // first we want to release the lock we have on this record
549       // TODO #refactor move this into model layer
550       this.value.releaseLock(options);
551     },
552     /**
553      @todo Document the save method.
554      */
555     save: function (options) {
556       options = options || {};
557       var that = this,
558         success = options.success,
559         inEvent = {
560           originator: this,
561           model: this.getModel(),
562           id: this.value.id,
563           done: options.modelChangeDone
564         };
565       options.success = function (model, resp, options) {
566         that.doModelChange(inEvent);
567         that.parent.parent.modelSaved();
568         if (that.callback) { that.callback(model); }
569         if (success) { success(model, resp, options); }
570       };
571       this.value.save(null, options);
572     },
573     /**
574       Set save text when it is changed
575     */
576     saveTextChanged: function () {
577       var inEvent = {
578         content: this.getSaveText()
579       };
580       this.doSaveTextChange(inEvent);
581     },
582     /**
583      @todo Document the statusChanged method.
584      */
585     statusChanged: function (model, status, options) {
586       options = options || {};
587       var inEvent = {model: model, status: status},
588         attrs = model.getAttributeNames(),
589         changes = {},
590         i,
591         dbName;
592
593       // Add to history if appropriate.
594       if (model.id && model.keepInHistory) {
595         XT.addToHistory(this.kind, model, function (historyArray) {
596           dbName = XT.session.details.organization;
597           enyo.setCookie("history_" + dbName, JSON.stringify(historyArray));
598         });
599         this.doHistoryChange(this);
600       }
601
602       // Update attributes
603       for (i = 0; i < attrs.length; i++) {
604         changes[attrs[i]] = true;
605       }
606       options.changes = changes;
607
608       // Update header if applicable
609       if (model.isReady()) {
610         this.headerValuesChanged();
611         this.attributesChanged(model, options);
612       }
613
614       this.doStatusChange(inEvent);
615     },
616     /**
617      This function sets the title widget in the workspace Toolbar to the title
618       specified in the Workspace specification. If one is not specified, then "_none" is used.
619      */
620     titleChanged: function () {
621       var inEvent = { title: this.getTitle(), originator: this };
622       this.doTitleChange(inEvent);
623     }
624   }));
625
626   /**
627     @name XV.WorkspaceContainer
628     @class Contains the navigation and content panels which wrap around a workspace.<br />
629     See also {@link XV.Workspace}.<br />
630     Derived from <a href="http://enyojs.com/api/#enyo.Panels">enyo.Panels</a>.
631     @extends enyo.Panels
632     @extends XV.ListMenuManagerMixin
633    */
634   enyo.kind(/** @lends XV.WorkspaceContainer# */{
635     name: "XV.WorkspaceContainer",
636     kind: "XV.ContainerPanels",
637     classes: "xv-workspace-container",
638     published: {
639       menuItems: [],
640       allowNew: true
641     },
642     events: {
643       onPrevious: "",
644       onNotify: ""
645     },
646     handlers: {
647       onClose: "closed",
648       onError: "errorNotify",
649       onHeaderChange: "headerChanged",
650       onHotKey: "handleHotKey",
651       onListItemMenuTap: "showListItemMenu",
652       onLockChange: "lockChanged",
653       onMenuChange: "menuChanged",
654       onNotify: "notify",
655       onPrint: "print",
656       onReport: "report",
657       onStatusChange: "statusChanged",
658       onTitleChange: "titleChanged",
659       onSaveTextChange: "saveTextChanged",
660       onExportAttr:     "exportAttr"
661     },
662     components: [
663       {kind: "FittableRows", name: "navigationPanel", classes: "xv-menu-container", components: [
664         {kind: "onyx.Toolbar", name: "menuToolbar", components: [
665           {kind: "font.TextIcon", name: "backButton",
666             content: "_back".loc(), ontap: "close", icon: "chevron-left"},
667           {kind: "onyx.MenuDecorator", onSelect: "actionSelected", components: [
668             {kind: "font.TextIcon", icon: "cog",
669               content: "_actions".loc(), name: "actionButton"},
670             {kind: "onyx.Menu", name: "actionMenu", floating: true}
671           ]}
672         ]},
673         {name: "loginInfo", classes: "xv-header"},
674         {name: "menu", kind: "List", fit: true, touch: true, classes: 'xv-navigator-menu',
675            onSetupItem: "setupItem", components: [
676           {name: "item", classes: "item enyo-border-box xv-list-item", ontap: "itemTap"}
677         ]}
678       ]},
679       {kind: "FittableRows", name: "contentPanel", classes: 'xv-content-panel', components: [
680         {kind: "onyx.MoreToolbar", name: "contentToolbar", components: [
681           {kind: "onyx.Grabber", classes: "spacer", unmoveable: true,},
682           {name: "title", classes: "xv-toolbar-label", unmoveable: true,},
683           {name: "space", classes: "spacer", fit: true},
684           {kind: "font.TextIcon", name: "lockImage", showing: false,
685             content: "Locked", ontap: "lockTapped", icon: "lock", classes: "lock"},
686           {kind: "font.TextIcon", name: "backPanelButton", unmoveable: true,
687             content: "_back".loc(), ontap: "close", icon: "chevron-left"},
688           {kind: "font.TextIcon", name: "refreshButton",
689             content: "_refresh".loc(), onclick: "requery", icon: "rotate-right"},
690           {kind: "font.TextIcon", name: "saveAndNewButton", disabled: false,
691             content: "_new".loc(), ontap: "saveAndNew", icon: "plus"},
692           {kind: "font.TextIcon", name: "applyButton", disabled: true,
693             content: "_apply".loc(), ontap: "apply", icon: "ok"},
694           {kind: "font.TextIcon", name: "saveButton",
695             disabled: true, icon: "save", classes: "save",
696             content: "_save".loc(), ontap: "saveAndClose"}
697         ]},
698         {name: "header", content: "_loading".loc(), classes: "xv-header"},
699         {kind: "onyx.Popup", name: "spinnerPopup", centered: true,
700           modal: true, floating: true, scrim: true,
701           onHide: "popupHidden", components: [
702           {kind: "onyx.Spinner"},
703           {name: "spinnerMessage", content: "_loading".loc() + "..."}
704         ]},
705         {kind: "onyx.Popup", name: "lockPopup", centered: true,
706           modal: true, floating: true, components: [
707           {name: "lockMessage", content: ""},
708           {tag: "br"},
709           {kind: "onyx.Button", content: "_ok".loc(), ontap: "lockOk",
710             classes: "onyx-blue xv-popup-button"}
711         ]},
712         {name: "listItemMenu", kind: "onyx.Menu", floating: true, onSelect: "listActionSelected",
713           maxHeight: 500, components: []
714         }
715       ]}
716     ],
717     actionSelected: function (inSender, inEvent) {
718       // Could have come from an action, or a an action button
719       var selected = inEvent.selected || inEvent.originator;
720
721       // If it's a view method then call function on the workspace.
722       if (selected.isViewMethod || selected.container.isViewMethod) {
723         this.$.workspace[selected.method || selected.container.method](inEvent);
724
725       // Otherwise call it on the workspace's model.
726       } else {
727         this.$.workspace.getValue()[selected.method]();
728       }
729     },
730     allowNewChanged: function () {
731       var allowNew = this.getAllowNew();
732       this.$.saveAndNewButton.setShowing(allowNew);
733     },
734     /**
735      @todo Document the apply method.
736      */
737     apply: function () {
738       this._saveState = SAVE_APPLY;
739       this.save();
740     },
741     askAboutUnsaved: function (shouldClose) {
742       var that = this,
743         message = "_unsavedChanges".loc() + " " + "_saveYourWork?".loc(),
744         callback = function (response) {
745           var answer = response.answer;
746
747           if (answer === true && shouldClose) {
748             that.saveAndClose({force: true});
749           } else if (answer === true) {
750             that.save();
751           } else if (answer === false && shouldClose) {
752             that.close({force: true});
753           } else if (answer === false) {
754             that.$.workspace.requery();
755           } // else answer === undefined means cancel, so do nothing
756         };
757       this.doNotify({
758         type: XM.Model.YES_NO_CANCEL,
759         message: message,
760         yesLabel: "_save".loc(),
761         noLabel: "_discard".loc(),
762         callback: callback
763       });
764     },
765     buildMenus: function () {
766       var actionMenu = this.$.actionMenu,
767         workspace = this.$.workspace,
768         actions = workspace.getActions(),
769         actionButtons = workspace.getActionButtons(),
770         model = workspace.getValue(),
771         that = this,
772         count = 0;
773
774       // Handle menu actions
775       if (actions) {
776
777         // Reset the menu
778         actionMenu.destroyClientControls();
779
780         // Add whatever actions are applicable to the current context.
781         _.each(actions, function (action) {
782           var name = action.name,
783             prerequisite = action.prerequisite,
784             privilege = action.privilege,
785             isDisabled = privilege ? !XT.session.privileges.get(privilege) : false;
786
787           // Only create menu item if prerequisites are met.
788           if (!prerequisite || model[prerequisite]()) {
789             actionMenu.createComponent({
790               name: name,
791               kind: XV.MenuItem,
792               content: action.label || ("_" + name).loc(),
793               method: action.method || action.action || name,
794               disabled: isDisabled,
795               isViewMethod: action.isViewMethod
796             });
797             count++;
798           }
799
800         });
801
802         if (actions.length && !count) {
803           actionMenu.createComponent({
804             name: "noActions",
805             kind: XV.MenuItem,
806             content: "_noEligibleActions".loc(),
807             disabled: true
808           });
809         }
810
811         actionMenu.render();
812       }
813       this.$.actionButton.setShowing(actions && actions.length);
814
815       // Handle button actions
816       if (actionButtons) {
817         _.each(actionButtons, function (action) {
818           var privs =  XT.session.privileges,
819             noPriv = action.privilege ? !privs.get(action.privilege): false,
820             noCanDo = action.prerequisite ? !model[action.prerequisite]() : false;
821
822           that.$[action.name].setDisabled(noPriv || noCanDo);
823         });
824       }
825     },
826     create: function () {
827       this.inherited(arguments);
828       this.setLoginInfo();
829     },
830     closed: function (inSender, inEvent) {
831       this.close(inEvent);
832     },
833     /**
834      Backs out of the workspace. This can be done using the back button, or
835      during the end of the save-and-close process.
836      */
837     close: function (options) {
838       var that = this,
839         workspace = this.$.workspace,
840         model = this.$.workspace.getValue();
841
842       options = options || {};
843       if (!options.force) {
844         if (workspace.getDirtyWarn() && workspace.isDirty()) {
845           this.askAboutUnsaved(true);
846           return;
847         }
848       }
849
850       if (workspace.value.getStatus() === XM.Model.READY_DIRTY &&
851          !workspace.getModelAmnesty()) {
852         // Revert because this model may be referenced elsewhere
853         _setBindings(this.$.workspace, "off");
854         model.revert();
855       }
856
857       if (model && model.hasLockKey && model.hasLockKey()) {
858         model.releaseLock({
859           success: function () {
860             that.doPrevious();
861           },
862           error: function () {
863             XT.log("Error releasing lock");
864             that.doPrevious();
865           }
866         });
867       } else {
868         that.doPrevious();
869       }
870     },
871     /**
872      @todo Document the destroyWorkspace method.
873      */
874     destroyWorkspace: function () {
875       var workspace = this.$.workspace;
876       if (workspace) {
877         this.removeComponent(workspace);
878         workspace.destroy();
879       }
880     },
881     /**
882       Legacy case for notify() function
883      */
884     errorNotify: function (inSender, inEvent) {
885       var message = inEvent.error.message ? inEvent.error.message() : "Error";
886       inEvent.message = message;
887       inEvent.type = XM.Model.CRITICAL;
888       this.doNotify(inEvent);
889     },
890     setLoginInfo: function () {
891       var details = XT.session.details;
892       this.$.loginInfo.setContent(details.username + " Â· " + details.organization);
893     },
894     handleHotKey: function (inSender, inEvent) {
895       var keyCode = inEvent.keyCode;
896
897       switch (String.fromCharCode(keyCode)) {
898       case 'A':
899         this.apply();
900         return;
901       case 'B':
902         this.close();
903         return;
904       case 'R':
905         this.requery();
906         return;
907       case 'S':
908         this.saveAndClose();
909         return;
910       }
911
912       // else see if the workspace has a specific implementation
913       this.$.workspace.handleHotKey(keyCode);
914     },
915     /**
916      @todo Document the headerChanged method.
917      */
918     headerChanged: function (inSender, inEvent) {
919       this.$.header.setContent(inEvent.header);
920       return true;
921     },
922     /**
923      @todo Document the itemTap method.
924      */
925     itemTap: function (inSender, inEvent) {
926       var workspace = this.$.workspace,
927         panel = this.getMenuItems()[inEvent.index],
928         prop,
929         i,
930         panels;
931       // Find the panel in the workspace and set it to current
932       // XXX let's find a better way to keep track of this
933       for (prop in workspace.$) {
934         if (workspace.$.hasOwnProperty(prop) &&
935             workspace.$[prop] instanceof enyo.Panels) {
936           panels = workspace.$[prop].getPanels();
937           for (i = 0; i < panels.length; i++) {
938             if (panels[i] === panel) {
939               workspace.$[prop].setIndex(i);
940               break;
941             }
942           }
943         }
944       }
945
946       // Mobile device view
947       if (enyo.Panels.isScreenNarrow()) {
948         this.next();
949       }
950     },
951     lockChanged: function (inSender, inEvent) {
952       this.$.lockImage.setShowing(!inEvent.hasKey);
953     },
954     lockOk: function () {
955       this.$.lockPopup.hide();
956     },
957     /**
958       If the user clicks on the lock icon, we tell them who got the lock and when
959      */
960     lockTapped: function () {
961       var lock = this.$.workspace.getValue().lock,
962         effective = Globalize.format(new Date(lock.effective), "t");
963       this.$.lockMessage.setContent("_lockInfo".loc()
964                                                .replace("{user}", lock.username)
965                                                .replace("{effective}", effective));
966       this.$.lockPopup.render();
967       this.$.lockPopup.show();
968     },
969     /**
970      Once a model has been saved we take our next action depending on
971      which of the save-and-X actions were actually requested. This
972      is part of the callback of the save operation.
973      */
974     modelSaved: function () {
975       if (this._saveState === SAVE_CLOSE) {
976         this.close();
977       } else if (this._saveState === SAVE_NEW) {
978         this.newRecord();
979       }
980     },
981     /**
982      @todo Document the newRecord method.
983      */
984     newRecord: function () {
985       this.$.workspace.newRecord();
986     },
987     /**
988       Although the main processing of the notify request happens
989       in XV.ModuleContainer, we do want to make sure that the spinner
990       goes away if it's present.
991      */
992     notify: function () {
993       this.spinnerHide();
994     },
995     /**
996      @todo Document the popupHidden method.
997      */
998     popupHidden: function (inSender, inEvent) {
999       if (!this._popupDone) {
1000         inEvent.originator.show();
1001       }
1002     },
1003     /**
1004      Refreshes the workspace.
1005      */
1006     requery: function (options) {
1007       options = options || {};
1008       if (!options.force) {
1009         if (this.$.workspace.isDirty()) {
1010           this.askAboutUnsaved(false);
1011           return;
1012         }
1013       }
1014       this.$.workspace.requery();
1015     },
1016     /**
1017       All the other save functions flow through here.
1018      */
1019     save: function (options) {
1020       var workspace = this.$.workspace,
1021         print = workspace.printOnSaveSetting &&
1022           XT.session.config.printAvailable &&
1023           XT.session.settings.get(workspace.printOnSaveSetting);
1024       if (!this._saveState) { this._saveState = SAVE_APPLY; }
1025       workspace.save(options);
1026
1027       // some workspaces are set up with a setting to have them automatically
1028       // print when they're saved.
1029       if (print) {
1030         this.$.workspace.print();
1031       }
1032     },
1033     /**
1034       Save the model and close out the workspace.
1035      */
1036     saveAndClose: function () {
1037       this._saveState = SAVE_CLOSE;
1038       this.save({requery: false});
1039     },
1040     /**
1041       Handles the save-and-new button click. Note that this button displays "new"
1042       if the model is clean, and we want it exhibit that conditional behavior.
1043      */
1044     saveAndNew: function () {
1045       if (this.$.workspace.getValue().isDirty()) {
1046         this._saveState = SAVE_NEW;
1047         this.save({requery: false});
1048       } else {
1049         this.newRecord();
1050       }
1051     },
1052
1053     saveTextChanged: function (inSender, inEvent) {
1054       this.$.saveButton.setContent(inEvent.content);
1055     },
1056
1057     exportAttr: function (inSender, inEvent) {
1058       this.openExportTab('export', inEvent.recordType, inEvent.uuid, inEvent.attr);
1059       return true;
1060     },
1061
1062     // export just one attribute of the model displayed by the workspace
1063     openExportTab: function (routeName, recordType, id, attr) {
1064       var query = { parameters: [{ attribute: "uuid", value: id }],
1065                     details: { attr: attr }
1066       };
1067       // sending the locale information back over the wire saves a call to the db
1068       window.open(XT.getOrganizationPath() +
1069         '/%@?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@,"print":%@}'
1070         .f(routeName,
1071           recordType.prefix(),
1072           recordType.suffix(),
1073           JSON.stringify(query),
1074           JSON.stringify(XT.locale.culture),
1075           "false"),
1076         '_newtab');
1077     },
1078     /**
1079      This is called for each row in the menu List.
1080      The menu text is derived from the corresponding panel index.
1081      If the panel is not visible, then the menu item is also not visible.
1082      */
1083     // menu
1084     setupItem: function (inSender, inEvent) {
1085       var box = this.getMenuItems()[inEvent.index],
1086         defaultTitle = "_overview".loc(),
1087         title = box.getTitle ? box.getTitle() ||
1088          defaultTitle : box.title ? box.title || defaultTitle : defaultTitle,
1089         visible = box.showing;
1090       this.$.item.setContent(title);
1091       this.$.item.box = box;
1092       this.$.item.addRemoveClass("onyx-selected", inSender.isSelected(inEvent.index));
1093       this.$.item.setShowing(visible);
1094
1095       return true;
1096     },
1097
1098     /**
1099       Loads a workspace into the workspace container.
1100       Accepts the following options:
1101         * workspace: class name (required)
1102         * id: record id to load. If none, a new record will be created.
1103         * allowNew: boolean indicating whether Save and New button is shown.
1104         * attributes: default attribute values for a new record.
1105         * success: function to call from the workspace when the workspace
1106           has either succefully fetched or created a model.
1107         * callback: function to call on either a successful save, or the user
1108           leaves the workspace without saving a new record. Passes the new or
1109           updated model as an argument.
1110     */
1111     setWorkspace: function (options) {
1112       var that = this,
1113         menuItems = [],
1114         prop,
1115         headerAttrs,
1116         workspace = options.workspace,
1117         id = options.id,
1118         callback = options.callback,
1119         // if the options do not specify allowNew, default it to true
1120         allowNew,
1121         attributes = options.attributes;
1122
1123       if (workspace) {
1124         this.destroyWorkspace();
1125         workspace = {
1126           name: "workspace",
1127           container: this.$.contentPanel,
1128           kind: workspace,
1129           fit: true,
1130           callback: callback
1131         };
1132
1133         workspace = this.createComponent(workspace);
1134
1135         // Handle save and new button
1136         allowNew = _.isBoolean(options.allowNew) ?
1137           options.allowNew : !workspace.getHideSaveAndNew();
1138         this.setAllowNew(allowNew);
1139
1140         headerAttrs = workspace.getHeaderAttrs() || [];
1141
1142         // Set the button texts
1143         this.$.saveButton.setContent(workspace.getSaveText());
1144         this.$.backButton.setContent(workspace.getBackText());
1145         this.$.backPanelButton.setContent(workspace.getBackText());
1146         this.$.backPanelButton.setShowing(enyo.Panels.isScreenNarrow());
1147
1148         // Hide buttons if applicable
1149         this.$.applyButton.setShowing(!workspace.getHideApply());
1150         this.$.refreshButton.setShowing(!workspace.getHideRefresh());
1151
1152         // Add any extra action buttons to the toolbar
1153         _.each(this.$.workspace.actionButtons, function (action) {
1154           var actionIcon = {kind: "font.TextIcon",
1155             name: action.name,
1156             content: action.label || ("_" + action.name).loc(),
1157             icon: action.icon,
1158             method: action.method || action.name,
1159             isViewMethod: action.isViewMethod,
1160             ontap: "actionSelected"};
1161           this.$.contentToolbar.createComponent(
1162             actionIcon, {owner: this});
1163         }, this);
1164         this.$.contentToolbar.resized();
1165
1166         this.render();
1167         if (id || id === false) {
1168           workspace.setSuccess(options.success);
1169           workspace.setRecordId(id);
1170         } else {
1171           workspace.newRecord(attributes, options);
1172         }
1173       }
1174
1175       // Build menu by finding all panels
1176       this.$.menu.setCount(0);
1177       for (prop in workspace.$) {
1178         if (workspace.$.hasOwnProperty(prop) &&
1179             workspace.$[prop] instanceof enyo.Panels) {
1180           menuItems = menuItems.concat(workspace.$[prop].getPanels());
1181         }
1182       }
1183       this.setMenuItems(menuItems);
1184       this.$.menu.setCount(menuItems.length);
1185       this.$.menu.render();
1186
1187       // Mobile device view
1188       if (enyo.Panels.isScreenNarrow()) {
1189         this.next();
1190       }
1191     },
1192     /**
1193      Hides the spinner popup
1194      */
1195     spinnerHide: function () {
1196       this._popupDone = true;
1197       this.$.spinnerPopup.hide();
1198     },
1199     /**
1200      Show the modal spinner popup when the model
1201      is busy.
1202      */
1203     spinnerShow: function (message) {
1204       message = message || "_loading".loc() + "...";
1205       this._popupDone = false;
1206       this.$.spinnerMessage.setContent(message);
1207       this.$.spinnerPopup.show();
1208     },
1209     /**
1210       This function is called by the statusChange handler and
1211       controls button and spinner functions based on the changed
1212       status of the backing model of the workspace.
1213      */
1214     statusChanged: function (inSender, inEvent) {
1215       var model = inEvent.model,
1216         K = XM.Model,
1217         status = inEvent.status,
1218         canCreate = model.getClass().canCreate(),
1219         canUpdate = model.canUpdate() || status === K.READY_NEW,
1220         isEditable = canUpdate && !model.isReadOnly(),
1221         canNotSave = !model.isDirty() || !isEditable,
1222         message;
1223
1224       // Status dictates whether buttons are actionable
1225       this.$.saveAndNewButton.setShowing(canCreate && this.getAllowNew());
1226       this.$.saveAndNewButton.setContent("_new".loc());
1227
1228       // XXX we really only have to do this if there's a *change* to the button content
1229       this.$.contentToolbar.render();
1230
1231       this.$.applyButton.setDisabled(canNotSave);
1232       this.$.saveAndNewButton.setDisabled(!isEditable);
1233       this.$.saveButton.setDisabled(canNotSave);
1234
1235       // Toggle spinner popup
1236       if (status & K.BUSY) {
1237         if (status === K.BUSY_COMMITTING) {
1238           message = "_saving".loc() + "...";
1239         }
1240         this.spinnerShow(message);
1241       } else {
1242         this.spinnerHide();
1243       }
1244
1245       this.buildMenus();
1246     },
1247     /**
1248      @todo Document titleChanged method.
1249      */
1250     titleChanged: function (inSender, inEvent) {
1251       var title = inEvent.title || "";
1252       this.$.title.setContent(title);
1253       return true;
1254     },
1255
1256     /**
1257     This function forces the menu to render and call
1258     its setup function for the List.
1259      */
1260     menuChanged: function () {
1261       this.$.menu.render();
1262     }
1263   });
1264
1265 }());