/*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true,
latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
trailing:true, white:true, strict: false*/
/*global XV:true, XM:true, _:true, enyo:true, XT:true, Globalize:true, window:true, console:true */
(function () {
var SAVE_APPLY = 1;
var SAVE_CLOSE = 2;
var SAVE_NEW = 3;
/**
@name XV.EditorMixin
@class A mixin that contains functionality common to {@link XV.Workspace}
and {@link XV.ListRelationsEditorBox}.
*/
/**
@name XV.EditorMixin
@class A mixin that contains functionality common to {@link XV.Workspace}
and {@link XV.ListRelationsEditorBox}.
*/
XV.EditorMixin = {
controlValueChanged: function (inSender, inEvent) {
var attrs = {},
attr = inEvent.originator.attr;
if (this.value && attr) {
// If the value is an object, then the object is already mapped
if (attr instanceof Object) {
attrs = inEvent.value;
// Otherwise map a basic value to its attribute
} else {
attrs[attr] = inEvent.value;
}
this.value.setValue(attrs);
return true;
} else if (XT.session.config.debugging) {
XT.log("Ignoring content-less model update event", this.value, attr);
}
},
/**
Updates all child controls on the editor where the name of
the control matches the name of an attribute on the model.
@param {XM.Model} model
@param {Object} options
*/
attributesChanged: function (model, options) {
// If the model is meta, then move up to the workspace model
if (!(model instanceof XM.Model)) { model = this.value; }
options = options || {};
var value,
K = XM.Model,
status = model.getStatus(),
canView = true,
canUpdate = (status === K.READY_NEW) ||
((status & K.READY) && model.canUpdate()),
isReadOnly = false,
isRequired,
prop,
modelPropName,
attribute,
obj,
ctrls = this.$;
// loop through each of the controls and set the value using the
// attribute on the control
_.each(ctrls, function (control) {
if (control.attr) {
if (!control.getAttr) {
XT.log("Warning: " + control.kind + " is not a valid XV control");
}
attribute = control.getAttr();
// for compound controls, the attribute is an object with key/value pairs.
if (_.isObject(attribute)) {
obj = _.clone(attribute);
// replace the current values in the object (attribute names)
// with the values from the model
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
prop = model.attributeDelegates && model.attributeDelegates[attr] ?
model.attributeDelegates[attr] : attr;
// replace the current value of the property name with the value from the model
modelPropName = obj[prop];
obj[prop] = model.getValue(modelPropName);
}
}
value = obj;
// only worry about the one mapping to isEditableProperty
if (control.isEditableProperty) {
attribute = attribute[control.isEditableProperty];
// If not isEditableProperty defined one is as good as another, just pick one
} else {
attribute = attribute[_.first(_.keys(attribute))];
}
}
prop = model.attributeDelegates && model.attributeDelegates[attribute] ?
model.attributeDelegates[attribute] : attribute;
if (!obj) { value = model.getValue(prop); }
canView = model.canView(prop);
isReadOnly = model.isReadOnly(prop) || !model.canEdit(prop);
isRequired = model.isRequired(prop);
if (canView) {
control.setShowing(true);
if (control.setPlaceholder && isRequired && !control.getPlaceholder()) {
control.setPlaceholder("_required".loc());
}
if (control.setValue && !(status & K.BUSY)) {
control.setValue(value, {silent: true});
}
if (control.setDisabled) {
control.setDisabled(!canUpdate || isReadOnly);
}
} else {
control.setShowing(false);
}
isReadOnly = false;
canView = true;
obj = undefined;
}
}, this);
},
/**
@todo Document the clear method.
*/
clear: function () {
var attrs = this.value ? this.value.getAttributeNames() : [],
attr,
control,
i;
for (i = 0; i < attrs.length; i++) {
attr = attrs[i];
control = this.findControl(attr);
if (control && control.clear) {
control.clear({silent: true});
}
}
},
/**
Returns the control that contains the attribute string.
*/
findControl: function (attr) {
return _.find(this.$, function (ctl) {
if (_.isObject(ctl.attr)) {
return _.find(ctl.attr, function (str) {
return str === attr;
});
}
return ctl.attr === attr;
});
},
/**
Bubble up an event to ask a question to the user. The user interaction
is handled by XV.ModuleContainer.
*/
notify: function (model, message, options) {
var inEvent = _.extend(options, {
originator: this,
model: model,
message: message
});
this.doNotify(inEvent);
}
};
/**
Set model bindings on a workspace
@private
@param{Object} Workspace
@param{String} Action: 'on' or 'off'
*/
var _setBindings = function (ws, action) {
var headerAttrs = ws.getHeaderAttrs() || [],
observers = "",
model = ws.value,
attr,
i;
model[action]("change", ws.attributesChanged, ws);
model[action]("lockChange", ws.lockChanged, ws);
model[action]("readOnlyChange", ws.attributesChanged, ws);
model[action]("statusChange", ws.statusChanged, ws);
model[action]("invalid", ws.error, ws);
model[action]("error", ws.error, ws);
model[action]("notify", ws.notify, ws);
if (headerAttrs.length) {
for (i = 0; i < headerAttrs.length; i++) {
attr = headerAttrs[i];
if (attr.indexOf('.') !== -1 ||
_.contains(model.getAttributeNames(), attr)) {
observers = observers ? observers + " change:" + attr : "change:" + attr;
}
}
model[action](observers, ws.headerValuesChanged, ws);
}
// If meta data exists, handle that too.
if (model.meta) {
model.meta[action]("change", ws.attributesChanged, ws);
}
};
XV.WorkspacePanelsRefactor = {
rendered: function () {
if (!this.$.panels || !this.$.panels.hasClass('xv-workspace-panel')) {
this.inherited(arguments);
return;
}
_.each(this.$.panels.getClientControls(), function (control) {
control.addClass('xv-workspace-panel');
});
this.$.panels.removeClass('xv-workspace-panel');
this.inherited(arguments);
}
};
/**
@name XV.Workspace
@class Contains a set of fittable rows which are laid out
using a collapsing arranger and fitted to the size of the viewport.<br />
Its components can be extended via {@link XV.ExtensionsMixin}.<br />
Derived from <a href="http://enyojs.com/api/#enyo.FittableRows">enyo.FittableRows</a>.
Supported properties on the action array are:
* name
* label: Menu label. Defaults to name if not present.
* privilege: The privilege required by the user to enable the menu. Defaults enabled if not present.
* prerequisite: A function on the model that returns a boolean dictating whether to show the menu item or not.
* method: The function to call. Defaults to name if not present.
* isViewMethod: Boolean value dictates whether method is called on the view or the view's model. Default false.
@extends XV.WorkspacePanels
@extends XV.EditorMixin
@extends XV.ExtensionsMixin
@see XV.WorkspaceContainer
*/
enyo.kind(_.extend({ },
XV.EditorMixin, XV.ExtensionsMixin,
XV.WorkspacePanelsRefactor, {
name: "XV.Workspace",
kind: "XV.WorkspacePanels",
classes: 'xv-workspace',
published: {
actions: null,
actionButtons: null,
title: "_none".loc(),
headerAttrs: null,
success: null,
callback: null,
modelAmnesty: false, // do we keep the model even if the workspace is destroyed?
// typically no, but yes for child workspaces
printOnSaveSetting: "", // some workspaces have a setting that cause them to be
// automatically printed upon saving
reportName: null,
reportModel: null,
recordId: null,
saveText: "_save".loc(),
backText: "_back".loc(),
hideSaveAndNew: false,
hideApply: false,
hideRefresh: false,
dirtyWarn: true,
/**
* The type of the backing model.
*/
model: null,
/**
* The backing model for this component.
* @see XM.EnyoView#model
*/
value: null,
/**
* @see XM.View#workspace.template
*/
template: null
},
extensions: null,
events: {
onClose: "",
onError: "",
onHeaderChange: "",
onModelChange: "",
onStatusChange: "",
onTitleChange: "",
onHistoryChange: "",
onLockChange: "",
onMenuChange: "",
onNotify: "",
onSaveTextChange: "",
onTransactionList: "",
onWorkspace: "",
onPrevious: ""
},
handlers: {
onValueChange: "controlValueChanged"
},
components: [
{kind: "XV.Groupbox", name: "mainPanel",
components: [
{kind: "onyx.GroupboxHeader", content: "_overview".loc()},
{kind: "XV.ScrollableGroupbox", name: "mainGroup",
classes: "in-panel", fit: true, components: [
{kind: "XV.InputWidget", attr: "name"},
{kind: "XV.InputWidget", attr: "description"}
]}
]}
],
create: function () {
this.inherited(arguments);
XM.View.setPresenter(this, 'workspace');
this.processExtensions();
this.titleChanged();
},
/**
@todo Document the destroy method.
*/
destroy: function () {
var model = this.getValue(),
modelAmnesty = this.getModelAmnesty(),
wasNew = model.isNew();
this.setRecordId(null);
// If we never saved a new model, make the callback
// so the caller can deal with that and destroy it.
if (wasNew || modelAmnesty) {
if (this.callback) { this.callback(false); }
}
this.inherited(arguments);
},
/**
@todo Document the error method.
*/
error: function (model, error) {
var inEvent = {
originator: this,
model: model,
error: error
};
this.doError(inEvent);
this.attributesChanged(this.getValue());
},
/**
@todo Document the fetch method.
*/
fetch: function (options) {
options = options || {};
var wsSuccess = this.getSuccess(),
success = options.success;
if (wsSuccess) {
wsSuccess = _.bind(wsSuccess, this);
}
options.success = function (model, resp, options) {
if (wsSuccess) { wsSuccess(model, resp, options); }
if (success) { success(model, resp, options); }
};
if (!this.value) { return; }
this.value.fetch(options);
},
/**
Implementation is up to subkinds
*/
handleHotKey: function (keyValue) {},
/**
@todo Document the headerValuesChanged method.
*/
headerValuesChanged: function () {
var headerAttrs = this.getHeaderAttrs() || [],
model = this.value,
header = "",
value,
attr,
i;
if (headerAttrs.length && model) {
for (i = 0; i < headerAttrs.length; i++) {
attr = headerAttrs[i];
if (attr.indexOf('.') !== -1 ||
_.contains(model.getAttributeNames(), attr)) {
value = model.getValue(headerAttrs[i]) || "";
header = header ? header + " " + value : value;
} else {
header = header ? header + " " + attr : attr;
}
}
}
this.doHeaderChange({originator: this, header: header });
},
/**
@todo Document the isDirty method.
*/
isDirty: function () {
return this.value ? this.value.isDirty() : false;
},
lockChanged: function () {
this.doLockChange({hasKey: this.getValue().hasLockKey()});
},
/**
@todo Document the newRecord method.
*/
newRecord: function (attributes, options) {
options = options || {};
var model = this.getModel(),
Klass = XT.getObjectByName(model),
attr,
changes = {},
that = this,
relOptions = {
success: function () {
that.attributesChanged(that.value);
}
},
// Update record id directly when we get it, but don't trigger changes
updateRecordId = function (model) {
that.recordId = model.id;
that.value.off("change:" + that.value.idAttribute, updateRecordId, that);
};
this.setRecordId(null);
this.value = new Klass();
_setBindings(this, "on");
this.value.on("change:" + this.value.idAttribute, updateRecordId, this);
this.clear();
this.headerValuesChanged();
this.value.initialize(null, {isNew: true});
if (options.success) { options.success.call(this); }
this.value.set(attributes, {force: true});
for (attr in attributes) {
if (attributes.hasOwnProperty(attr)) {
this.value.setReadOnly(attr);
if (this.value.getRelation(attr)) {
this.value.fetchRelated(attr, relOptions);
} else {
changes[attr] = true;
this.attributesChanged(this.value, {changes: changes});
}
}
}
},
download: function () {
this.openReport(XT.getOrganizationPath() + this.getValue().getReportUrl("download"));
},
/**
Email the model's data, either silently or by opening a tab
*/
email: function () {
if (XT.session.config.emailAvailable) {
// send it to be printed silently by the server
this.getValue().doEmail();
} else {
this.openReport(XT.getOrganizationPath() + this.getValue().getReportUrl());
}
},
/**
If a printer is available, print the report silently through the model. Otherwise, show the report in
another tab.
*/
print: function (options) {
var that = this,
model = options.model || this.getValue(),
printer = options.printer || XT.defaultPrinter(model.recordType.suffix()) || null,
printQty = options.printQty || model.get(model.quantityAttribute),
rptParams,
reportPayload;
// reportName and customerForm may be defined in the list's print action
model.reportName = options.reportName || null;
model.custFormType = options.custFormType || null;
if (!model.getPrintParameters) {
return this.doNotify({message: "_modelMissingMethod".loc() + ": getPrintParameters"});
}
var doPrint = function (params) {
// Print to Printer
if (printer && printer !== "Browser") {
rptParams = [];
// Assemble the report params how the route wants them i.e.:
// [sohead_id::integer=3196, hide closed::boolean=true]
_.each(params.printParameters, function (param) {
rptParams.push("%@::%@=%@".f(param.name, param.type, param.value));
});
reportPayload = {
nameSpace: "ORPT",
type: params.reportName,
param: rptParams,
action: "print",
printer: printer,
id: params.id,
printQty: printQty || 1
};
XT.dataSource.callRoute("generate-report", reportPayload, {
success: function () {
// TODO: user may want a notify popup (document type, # and printer) on success.
console.log("print success");
if (options.success) { options.success(true); }
},
error: function (error) {
console.log("Error: " + error);
}
});
} else {// Print to Browser
var reportUrl = "/generate-report?nameSpace=%@&type=%@".f("ORPT", params.reportName);
// Assemble the report params to pass in the url path
rptParams = "";
_.each(params.printParameters, function (param) {
return rptParams += "¶m=%@::%@=%@".f(param.name, param.type, param.value);
});
that.openReport(XT.getOrganizationPath() + reportUrl + rptParams);
}
};
// Get the appropriate report parameters from the model and pass them to the print function
model.getPrintParameters(function (resp) {
if (!resp || resp.error) {
return that.doNotify({message: "Error: " + resp.error});
}
doPrint(resp);
});
},
/**
Open the report pdf in a new tab
*/
openReport: function (path) {
window.open(path, "_newtab");
},
/**
Handle clearing and reseting of model if the record id changes.
*/
recordIdChanged: function () {
var model = this.getModel(),
Klass = model ? XT.getObjectByName(model) : null,
recordId = this.getRecordId(),
attrs = {};
// Clean up
if (this.value) {
_setBindings(this, "off");
this.value.releaseLock();
}
// the configuration workspaces, notably, need to be fetched despite
// having an id of false. It works because the XM.Settings models use
// a dispatch for fetch.
if (recordId === undefined || recordId === null) {
if (this.value.isNew() && !this.modelAmnesty) {
this.value.destroy();
}
this.value = null;
return;
}
// Create new instance and bindings
if (recordId === false && this.singletonModel) {
this.setValue(XT.getObjectByName(this.singletonModel));
} else {
attrs[Klass.prototype.idAttribute] = recordId;
this.setValue(Klass.findOrCreate(attrs));
XM.backboneRouter.navigate("workspace/" +
this.value.recordType.substring(3).decamelize().replace(/_/g, "-") +
"/" + recordId);
}
_setBindings(this, "on");
this.fetch();
},
/**
Refresh the model behind this workspace. Note that we
have to release the lock first.
If this is being called on a new model, it means we
want to throw away the data in the model and start with
a new record. We don't worry about the lock in this case.
*/
requery: function () {
if (this.getValue().isNew()) {
this.newRecord();
return;
}
// model has already been saved
var that = this,
options = {
success: function () {
that.fetch();
},
error: function () {
XT.log("Error releasing lock.");
// fetch anyway. Why not!?
that.fetch();
}
};
// first we want to release the lock we have on this record
// TODO #refactor move this into model layer
this.value.releaseLock(options);
},
/**
@todo Document the save method.
*/
save: function (options) {
options = options || {};
var that = this,
printOnSave = this.$.printOnSave ? this.$.printOnSave.getValue() :
XT.session.settings.get(this.printOnSaveSetting),
success = options.success,
inEvent = {
originator: this,
model: this.getModel(),
id: this.value.id,
done: options.modelChangeDone
};
options.success = function (model, resp, options) {
that.doModelChange(inEvent);
that.parent.parent.modelSaved();
if (that.callback) { that.callback(model); }
if (success) { success(model, resp, options); }
// Some workspaces are set up with a setting to have them auto print when they're saved.
// The printOnSaveSetting set on workspace *OR* set in metrics.
if (printOnSave) {
that.print({model: model});
}
};
this.value.save(null, options);
},
/**
Set save text when it is changed
*/
saveTextChanged: function () {
var inEvent = {
content: this.getSaveText()
};
this.doSaveTextChange(inEvent);
},
/**
@todo Document the statusChanged method.
*/
statusChanged: function (model, status, options) {
options = options || {};
var inEvent = {model: model, status: status},
attrs = model.getAttributeNames(),
changes = {},
i,
dbName;
// Add to history if appropriate.
if (model.id && model.keepInHistory) {
XT.addToHistory(this.kind, model, function (historyArray) {
dbName = XT.session.details.organization;
enyo.setCookie("history_" + dbName, JSON.stringify(historyArray));
});
this.doHistoryChange(this);
}
// Update attributes
for (i = 0; i < attrs.length; i++) {
changes[attrs[i]] = true;
}
options.changes = changes;
// Update header if applicable
if (model.isReady()) {
this.headerValuesChanged();
this.attributesChanged(model, options);
}
this.doStatusChange(inEvent);
},
/**
This function sets the title widget in the workspace Toolbar to the title
specified in the Workspace specification. If one is not specified, then "_none" is used.
*/
titleChanged: function () {
var inEvent = { title: this.getTitle(), originator: this };
this.doTitleChange(inEvent);
}
}));
/**
@name XV.WorkspaceContainer
@class Contains the navigation and content panels which wrap around a workspace.<br />
See also {@link XV.Workspace}.<br />
Derived from <a href="http://enyojs.com/api/#enyo.Panels">enyo.Panels</a>.
@extends enyo.Panels
@extends XV.ListMenuManagerMixin
*/
enyo.kind(/** @lends XV.WorkspaceContainer# */_.extend({
name: "XV.WorkspaceContainer",
kind: "XV.ContainerPanels",
classes: "xv-workspace-container",
published: {
menuItems: [],
allowNew: true
},
events: {
onPrevious: "",
onNotify: ""
},
handlers: {
onClose: "closed",
onError: "errorNotify",
onHeaderChange: "headerChanged",
onHotKey: "handleHotKey",
onListItemMenuTap: "showListItemMenu",
onLockChange: "lockChanged",
onMenuChange: "menuChanged",
onNotify: "notify",
onPrint: "print",
onReport: "report",
onStatusChange: "statusChanged",
onTitleChange: "titleChanged",
onSaveTextChange: "saveTextChanged",
onExportAttr: "exportAttr"
},
components: [
{kind: "FittableRows", name: "navigationPanel", classes: "xv-menu-container", components: [
{kind: "onyx.Toolbar", name: "menuToolbar", components: [
{kind: "font.TextIcon", name: "backButton",
content: "_back".loc(), ontap: "close", icon: "chevron-left"},
{kind: "onyx.MenuDecorator", onSelect: "actionSelected", components: [
{kind: "font.TextIcon", icon: "cog",
content: "_actions".loc(), name: "actionButton"},
{kind: "onyx.Menu", name: "actionMenu", floating: true}
]}
]},
{name: "loginInfo", classes: "xv-header"},
{name: "menu", kind: "List", fit: true, touch: true, classes: 'xv-navigator-menu',
onSetupItem: "setupItem", components: [
{name: "item", classes: "item enyo-border-box xv-list-item", ontap: "itemTap"}
]}
]},
{kind: "FittableRows", name: "contentPanel", classes: 'xv-content-panel', components: [
{kind: "onyx.MoreToolbar", name: "contentToolbar", components: [
{kind: "onyx.Grabber", classes: "spacer", unmoveable: true,},
{name: "title", classes: "xv-toolbar-label", unmoveable: true,},
{name: "space", classes: "spacer", fit: true},
{kind: "font.TextIcon", name: "lockImage", showing: false,
content: "Locked", ontap: "lockTapped", icon: "lock", classes: "lock"},
{kind: "font.TextIcon", name: "backPanelButton", unmoveable: true,
content: "_back".loc(), ontap: "close", icon: "chevron-left"},
{kind: "font.TextIcon", name: "refreshButton",
content: "_refresh".loc(), onclick: "requery", icon: "rotate-right"},
{kind: "font.TextIcon", name: "saveAndNewButton", disabled: false,
content: "_new".loc(), ontap: "saveAndNew", icon: "plus"},
{kind: "font.TextIcon", name: "applyButton", disabled: true,
content: "_apply".loc(), ontap: "apply", icon: "ok"},
{kind: "font.TextIcon", name: "saveButton",
disabled: true, icon: "save", classes: "save",
content: "_save".loc(), ontap: "saveAndClose"}
]},
{name: "header", content: "_loading".loc(), classes: "xv-header"},
{kind: "onyx.Popup", name: "spinnerPopup", centered: true,
modal: true, floating: true, scrim: true,
onHide: "popupHidden", components: [
{kind: "onyx.Spinner"},
{name: "spinnerMessage", content: "_loading".loc() + "..."}
]},
{kind: "onyx.Popup", name: "lockPopup", centered: true,
modal: true, floating: true, components: [
{name: "lockMessage", content: ""},
{tag: "br"},
{kind: "onyx.Button", content: "_ok".loc(), ontap: "lockOk",
classes: "onyx-blue xv-popup-button"}
]},
{name: "listItemMenu", kind: "onyx.Menu", floating: true, onSelect: "listActionSelected",
maxHeight: 500, components: []
}
]}
],
actionSelected: function (inSender, inEvent) {
// Could have come from an action, or a an action button
var selected = inEvent.selected || inEvent.originator;
// If it's a view method then call function on the workspace.
if (selected.isViewMethod || selected.container.isViewMethod) {
this.$.workspace[selected.method || selected.container.method](inEvent);
// Otherwise call it on the workspace's model.
} else {
this.$.workspace.getValue()[selected.method]();
}
},
allowNewChanged: function () {
var allowNew = this.getAllowNew();
this.$.saveAndNewButton.setShowing(allowNew);
},
/**
@todo Document the apply method.
*/
apply: function () {
this._saveState = SAVE_APPLY;
this.save();
},
askAboutUnsaved: function (shouldClose) {
var that = this,
message = "_unsavedChanges".loc() + " " + "_saveYourWork?".loc(),
callback = function (response) {
var answer = response.answer;
if (answer === true && shouldClose) {
that.saveAndClose({force: true});
} else if (answer === true) {
that.save();
} else if (answer === false && shouldClose) {
that.close({force: true});
} else if (answer === false) {
that.$.workspace.requery();
} // else answer === undefined means cancel, so do nothing
};
this.doNotify({
type: XM.Model.YES_NO_CANCEL,
message: message,
yesLabel: "_save".loc(),
noLabel: "_discard".loc(),
callback: callback
});
},
buildMenus: function () {
var actionMenu = this.$.actionMenu,
workspace = this.$.workspace,
actions = workspace.getActions(),
actionButtons = workspace.getActionButtons(),
model = workspace.getValue(),
that = this,
count = 0;
// Handle menu actions
if (actions) {
// Reset the menu
actionMenu.destroyClientControls();
// Add whatever actions are applicable to the current context.
_.each(actions, function (action) {
var name = action.name,
prerequisite = action.prerequisite,
privilege = action.privilege,
isDisabled = privilege ? !XT.session.privileges.get(privilege) : false;
// Only create menu item if prerequisites are met.
if (!prerequisite || model[prerequisite]()) {
actionMenu.createComponent({
name: name,
kind: XV.MenuItem,
content: action.label || ("_" + name).loc(),
method: action.method || action.action || name,
disabled: isDisabled,
isViewMethod: action.isViewMethod
});
count++;
}
});
if (actions.length && !count) {
actionMenu.createComponent({
name: "noActions",
kind: XV.MenuItem,
content: "_noEligibleActions".loc(),
disabled: true
});
}
actionMenu.render();
}
this.$.actionButton.setShowing(actions && actions.length);
// Handle button actions
if (actionButtons) {
_.each(actionButtons, function (action) {
var privs = XT.session.privileges,
noPriv = action.privilege ? !privs.get(action.privilege): false,
noCanDo = action.prerequisite ? !model[action.prerequisite]() : false;
that.$[action.name].setDisabled(noPriv || noCanDo);
});
}
},
create: function () {
this.inherited(arguments);
this.setLoginInfo();
},
closed: function (inSender, inEvent) {
this.close(inEvent);
},
/**
Backs out of the workspace. This can be done using the back button, or
during the end of the save-and-close process.
*/
close: function (options) {
var that = this,
workspace = this.$.workspace,
model = this.$.workspace.getValue();
options = options || {};
if (!options.force) {
if (workspace.getDirtyWarn() && workspace.isDirty()) {
this.askAboutUnsaved(true);
return;
}
}
if (workspace.value.getStatus() === XM.Model.READY_DIRTY &&
!workspace.getModelAmnesty()) {
// Revert because this model may be referenced elsewhere
_setBindings(this.$.workspace, "off");
model.revert();
}
XM.backboneRouter.navigate("");
if (model && model.hasLockKey && model.hasLockKey()) {
model.releaseLock({
success: function () {
that.doPrevious();
},
error: function () {
XT.log("Error releasing lock");
that.doPrevious();
}
});
} else {
that.doPrevious();
}
},
/**
@todo Document the destroyWorkspace method.
*/
destroyWorkspace: function () {
var workspace = this.$.workspace;
if (workspace) {
this.removeComponent(workspace);
workspace.destroy();
}
},
/**
Legacy case for notify() function
*/
errorNotify: function (inSender, inEvent) {
var message = inEvent.error.message ? inEvent.error.message() : "Error";
inEvent.message = message;
inEvent.type = XM.Model.CRITICAL;
this.doNotify(inEvent);
},
setLoginInfo: function () {
var details = XT.session.details;
this.$.loginInfo.setContent(details.username + " · " + details.organization);
},
handleHotKey: function (inSender, inEvent) {
var keyCode = inEvent.keyCode;
switch (String.fromCharCode(keyCode)) {
case 'A':
this.apply();
return;
case 'B':
this.close();
return;
case 'R':
this.requery();
return;
case 'S':
this.saveAndClose();
return;
}
// else see if the workspace has a specific implementation
this.$.workspace.handleHotKey(keyCode);
},
/**
@todo Document the headerChanged method.
*/
headerChanged: function (inSender, inEvent) {
this.$.header.setContent(inEvent.header);
return true;
},
/**
@todo Document the itemTap method.
*/
itemTap: function (inSender, inEvent) {
var workspace = this.$.workspace,
panel = this.getMenuItems()[inEvent.index],
prop,
i,
panels;
// Find the panel in the workspace and set it to current
// XXX let's find a better way to keep track of this
for (prop in workspace.$) {
if (workspace.$.hasOwnProperty(prop) &&
workspace.$[prop] instanceof enyo.Panels) {
panels = workspace.$[prop].getPanels();
for (i = 0; i < panels.length; i++) {
if (panels[i] === panel) {
workspace.$[prop].setIndex(i);
break;
}
}
}
}
// Mobile device view
if (enyo.Panels.isScreenNarrow()) {
this.next();
}
},
lockChanged: function (inSender, inEvent) {
this.$.lockImage.setShowing(!inEvent.hasKey);
},
lockOk: function () {
this.$.lockPopup.hide();
},
/**
If the user clicks on the lock icon, we tell them who got the lock and when
*/
lockTapped: function () {
var lock = this.$.workspace.getValue().lock,
effective = Globalize.format(new Date(lock.effective), "t");
this.$.lockMessage.setContent("_lockInfo".loc()
.replace("{user}", lock.username)
.replace("{effective}", effective));
this.$.lockPopup.render();
this.$.lockPopup.show();
},
/**
Once a model has been saved we take our next action depending on
which of the save-and-X actions were actually requested. This
is part of the callback of the save operation.
*/
modelSaved: function () {
if (this._saveState === SAVE_CLOSE) {
this.close();
} else if (this._saveState === SAVE_NEW) {
this.newRecord();
}
},
/**
@todo Document the newRecord method.
*/
newRecord: function () {
this.$.workspace.newRecord();
},
/**
Although the main processing of the notify request happens
in XV.ModuleContainer, we do want to make sure that the spinner
goes away if it's present.
*/
notify: function () {
this.spinnerHide();
},
/**
@todo Document the popupHidden method.
*/
popupHidden: function (inSender, inEvent) {
if (!this._popupDone) {
inEvent.originator.show();
}
},
/**
Refreshes the workspace.
*/
requery: function (options) {
options = options || {};
if (!options.force) {
if (this.$.workspace.isDirty()) {
this.askAboutUnsaved(false);
return;
}
}
this.$.workspace.requery();
},
/**
All the other save functions flow through here.
*/
save: function (options) {
var workspace = this.$.workspace;
if (!this._saveState) { this._saveState = SAVE_APPLY; }
workspace.save(options);
},
/**
Save the model and close out the workspace.
*/
saveAndClose: function () {
this._saveState = SAVE_CLOSE;
this.save({requery: false});
},
/**
Handles the save-and-new button click. Note that this button displays "new"
if the model is clean, and we want it exhibit that conditional behavior.
*/
saveAndNew: function () {
if (this.$.workspace.getValue().isDirty()) {
this._saveState = SAVE_NEW;
this.save({requery: false});
} else {
this.newRecord();
}
},
saveTextChanged: function (inSender, inEvent) {
this.$.saveButton.setContent(inEvent.content);
},
exportAttr: function (inSender, inEvent) {
this.openExportTab('export', inEvent.attr);
return true;
},
// export just one attribute of the model displayed by the workspace
openExportTab: function (routeName, attr) {
var recordType = this.$.workspace.value.recordType,
id = this.$.workspace.value.id,
idAttr = this.$.workspace.value.idAttribute,
query = { parameters: [{ attribute: idAttr, value: id }],
details: { attr: attr, id: id }
};
// sending the locale information back over the wire saves a call to the db
window.open(XT.getOrganizationPath() +
'/%@?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@,"print":%@}'
.f(routeName,
recordType.prefix(),
recordType.suffix(),
JSON.stringify(query),
JSON.stringify(XT.locale.culture),
"false"),
'_newtab');
},
/**
This is called for each row in the menu List.
The menu text is derived from the corresponding panel index.
If the panel is not visible, then the menu item is also not visible.
*/
// menu
setupItem: function (inSender, inEvent) {
var box = this.getMenuItems()[inEvent.index],
defaultTitle = "_overview".loc(),
title = box.getTitle ? box.getTitle() ||
defaultTitle : box.title ? box.title || defaultTitle : defaultTitle,
visible = box.showing;
this.$.item.setContent(title);
this.$.item.box = box;
this.$.item.addRemoveClass("onyx-selected", inSender.isSelected(inEvent.index));
this.$.item.setShowing(visible);
return true;
},
/**
Loads a workspace into the workspace container.
Accepts the following options:
* workspace: class name (required)
* id: record id to load. If none, a new record will be created.
* allowNew: boolean indicating whether Save and New button is shown.
* attributes: default attribute values for a new record.
* success: function to call from the workspace when the workspace
has either succefully fetched or created a model.
* callback: function to call on either a successful save, or the user
leaves the workspace without saving a new record. Passes the new or
updated model as an argument.
*/
setWorkspace: function (options) {
var that = this,
menuItems = [],
prop,
headerAttrs,
workspace = options.workspace,
id = options.id,
callback = options.callback,
// if the options do not specify allowNew, default it to true
allowNew,
attributes = options.attributes;
if (workspace) {
this.destroyWorkspace();
workspace = {
name: "workspace",
container: this.$.contentPanel,
kind: workspace,
fit: true,
callback: callback
};
workspace = this.createComponent(workspace);
// Handle save and new button
allowNew = _.isBoolean(options.allowNew) ?
options.allowNew : !workspace.getHideSaveAndNew();
this.setAllowNew(allowNew);
headerAttrs = workspace.getHeaderAttrs() || [];
// Set the button texts
this.$.saveButton.setContent(workspace.getSaveText());
this.$.backButton.setContent(workspace.getBackText());
this.$.backPanelButton.setContent(workspace.getBackText());
this.$.backPanelButton.setShowing(enyo.Panels.isScreenNarrow());
// Hide buttons if applicable
this.$.applyButton.setShowing(!workspace.getHideApply() && !enyo.Panels.isScreenNarrow());
this.$.refreshButton.setShowing(!workspace.getHideRefresh() && !enyo.Panels.isScreenNarrow());
// Add any extra action buttons to the toolbar
_.each(this.$.workspace.actionButtons, function (action) {
var actionIcon = {kind: "font.TextIcon",
name: action.name,
content: action.label || ("_" + action.name).loc(),
icon: action.icon,
method: action.method || action.name,
isViewMethod: action.isViewMethod,
ontap: "actionSelected"};
this.$.contentToolbar.createComponent(
actionIcon, {owner: this});
}, this);
this.$.contentToolbar.resized();
this.render();
if (id || id === false) {
workspace.setSuccess(options.success);
workspace.setRecordId(id);
} else {
workspace.newRecord(attributes, options);
}
}
// Build menu by finding all panels
this.$.menu.setCount(0);
for (prop in workspace.$) {
if (workspace.$.hasOwnProperty(prop) &&
workspace.$[prop] instanceof enyo.Panels) {
menuItems = menuItems.concat(workspace.$[prop].getPanels());
}
}
this.setMenuItems(menuItems);
this.$.menu.setCount(menuItems.length);
this.$.menu.render();
// Mobile device view
if (enyo.Panels.isScreenNarrow()) {
this.next();
}
},
/**
Hides the spinner popup
*/
spinnerHide: function () {
this._popupDone = true;
this.$.spinnerPopup.hide();
},
/**
Show the modal spinner popup when the model
is busy.
*/
spinnerShow: function (message) {
message = message || "_loading".loc() + "...";
this._popupDone = false;
this.$.spinnerMessage.setContent(message);
this.$.spinnerPopup.show();
},
/**
This function is called by the statusChange handler and
controls button and spinner functions based on the changed
status of the backing model of the workspace.
*/
statusChanged: function (inSender, inEvent) {
var model = inEvent.model,
K = XM.Model,
status = inEvent.status,
canCreate = model.getClass().canCreate(),
canUpdate = model.canUpdate() || status === K.READY_NEW,
isEditable = canUpdate && !model.isReadOnly(),
canNotSave = !model.isDirty() || !isEditable,
message;
// Status dictates whether buttons are actionable
this.$.saveAndNewButton.setShowing(canCreate && this.getAllowNew());
this.$.saveAndNewButton.setContent("_new".loc());
// XXX we really only have to do this if there's a *change* to the button content
this.$.contentToolbar.render();
this.$.applyButton.setDisabled(canNotSave);
this.$.saveAndNewButton.setDisabled(!isEditable);
this.$.saveButton.setDisabled(canNotSave);
// Toggle spinner popup
if (status & K.BUSY) {
if (status === K.BUSY_COMMITTING) {
message = "_saving".loc() + "...";
}
this.spinnerShow(message);
} else {
this.spinnerHide();
}
this.buildMenus();
},
/**
@todo Document titleChanged method.
*/
titleChanged: function (inSender, inEvent) {
var title = inEvent.title || "";
this.$.title.setContent(title);
return true;
},
/**
This function forces the menu to render and call
its setup function for the List.
*/
menuChanged: function () {
this.$.menu.render();
}
}, XV.ListMenuManagerMixin));
}());