In InforCRM 8 the activity editor has been completely reworked to be entirely Javascript based. Although this brought a little bit of pain as none of the previous customizations carried over, and there wasn’t a whole lot of documentation available, it’s a big improvement over the last screen in term of how it makes possible modular customizations – where our changes can plug into the existing form instead of overwriting it.
To do that, we take advantage of the dynamic nature of Javascript to modify the way the ActivityEditor and ActivityService javascript classes methods work – adding bits that will implement our custom functionality. Once you know which method to modify, you can use dojo.aspect to add your own method that will run after the stock one is called. Because that can be called repeatedly it will still work if different modules add their own customizations to the form, effectively enabling third party modules that include changes to the activity form without requiring the customer to manually edit them (previously this was impossible because the ascx and ascx.cs files had to be edited directly – there was no provision to add functionality to them without that).
A common customization request is adding an activity capability to a custom entity. Adding the tab itself is easy enough – once you add the column to the activity table and update the entity in App Architect to reflect the change it will detect it and populate the tab automatically, so you just need to drop the tab’s smart part onto the custom entity page. The other customizations require us to load a Javascript module which will implement them. This itself can be done by modifying the base.master file, but ideally implemented instead via a .NET module defined in your business rules assembly and registered with the portal in Application Architect. For example this is my “ScriptOutputModule”:
public class ScriptOutputModule : IModule { public void Load() { Page page = HttpContext.Current.Handler as Page; if (page == null) return; ScriptManager.RegisterStartupScript(page, page.GetType(), "SSS_SharedScript", "require({ packages: [{ name: 'SSS', location: '" + HttpContext.Current.Request.ApplicationPath + "/SSS/js'}] }, ['SSS/Activity/ActivityEditor'], function(ActivityEditor) { ActivityEditor() });", true); } }
The Javascript starts simply like this:
define(['dojo/ready', 'dojo/_base/declare', 'dojo/aspect', 'dojo/_base/lang', 'Sage/MainView/ActivityMgr/ActivityEditor', 'Sage/MainView/ActivityMgr/HistoryEditor', 'Sage/Services/ActivityService', 'Sage/UI/Controls/Lookup'], function (ready, declare, aspect, lang, ActivityEditor, HistoryEditor, ActivityService, Lookup) { // this is where our custom code will go function initializeActivityEditor() { // ... } return function() { // This is invoked when the file is initially loaded - here we'll plug our custom code // into the activity editor initializeActivityEditor(); } });
Adding a lookup to the form is not a problem, the trickiest part is figuring out how to declare the lookup control in Javascript but that can pretty much be copy/pasted from the stock file (ActivityEditor.js – look for the “CreateAccountLookup” function). For example this shows how to add a SalesOrder lookup to the form. Note that it is important to understand the scope in which each function is invoked (usually the activity editor itself):
// create SalesOrder lookup (same function for activity & history editor) function createSalesOrderLookup(lookupId) { var lookupConfig = { isModal: true, id: lookupId + '_config', displayMode: 'Dialog', structure: [{ cells: [ { field: 'SalesOrderNumber', name: 'Order #' }, { field: 'Account.AccountName', name: 'Customer' }, { field: 'Status', name: 'Status' } ] }], gridOptions: { contextualCondition: '', contextualShow: '', selectionMode: 'single' }, storeOptions: { resourceKind: 'salesOrders', sort: [{ attribute: 'SalesOrderNumber'}] }, preFilters: [ ], seedProperty: 'Account.Id', dialogTitle: 'SalesOrder', dialogButtonText: 'OK' }; var lueSalesOrder = new Lookup({ id: lookupId, // This ID is required or Sage will not properly register the widgets allowClearingResult: true, label: 'SalesOrder', readonly: true, config: lookupConfig }); lueSalesOrder.textbox.required = false; this.eventConnections.push(dojo.connect(lueSalesOrder, 'onChange', dojo.hitch(this, function (sel) { if (this._isBinding) { return; } // here we add the selected salesorderid to the form's data var act = this._activityData || this._historyData; if (sel) { act.SalesOrderId = sel.$key; act.SalesOrderName = sel.$descriptor; // here we could add handling to automatically set or clear the account when they select a new sales order } else { act.SalesOrderId = null; act.SalesOrderName = null; } }))); return lueSalesOrder; } // helper to add lookups within a div, the reason is that the stylesheet for the activity dialog will resize immediate // descendants (with class textcontrol) of the td to 90%, by adding the div we prevent that from happening function addLookups(container, lookups) { for (var i = 0; i < lookups.length; i++) { var lup = lookups[i]; var div = new dijit.layout.ContentPane({ "class": "remove-padding lookup-container", label: lup.label }); dojo.place(lup.domNode, div.domNode, "only"); container.addChild(div); } // force restart container._initialized = false; container._started = false; container.startup() } // binding for the SalesOrder lookup (this is shared between activity & history editors) function manualBind() { this._isBinding = true; var act = this._activityData || this._historyData; this.sss_lueSalesOrder.set('selectedObject', act.SalesOrderId ? { $key: act.SalesOrderId, $descriptor: act.SalesOrderName} : null); this._isBinding = false; } // create custom lookups for Salesorder function createActivityLookups() { if (this.sss_lueSalesOrder) return; this.sss_lueSalesOrder = createSalesOrderLookup.call(this, 'activitySssSalesOrder_Lookup'); addLookups(this.contactContainer, [this.sss_lueSalesOrder]); }
And the last piece of the puzzle here – how do we get this createActivityLookups method to be called by the activity screen? Quite simply by piggy-backing on the method used on the stock screen to create the lookups, _ensureLookupsCreated (and _manualBind, which is used to populate the form when loaded for an edit):
function initializeActivityEditor() { aspect.after(ActivityEditor.prototype, "_ensureLookupsCreated", createActivityLookups); aspect.after(ActivityEditor.prototype, "_manualBind", manualBind); }
By using aspect.after, we ensure that multiple customizations can be registered and co-exist on the form in harmony. Note, for the history editor, there is no _ensureLookupsCreated method, so I just use “createAccountLookup” instead.
Providing the default context (so that the currently edited sales order is pre-selected on the activity screen) is slightly trickier because of the way that method is implemented but can still be accomplished using another method of dojo.aspect:
// retrieve default context for the sales order and pass it to the callback. // return true if context could be obtained, false otherwise function getDefaultContext(scope, callback) { var ctxSvc = Sage.Services.getService('ClientEntityContext'); if (!ctxSvc) return false; var ctx = ctxSvc.getContext(); if (!ctx) return false; if (ctx.EntityType == "Sage.Entity.Interfaces.ISalesOrder") { // ... maybe get the account / contact for the sales order, too? callback(scope, { SalesOrderId: ctx.EntityId, SalesOrderName: ctx.Description }); return true; } return false; } function initializeActivityEditor() { // ... aspect.around(ActivityService.prototype, "getActivityEntityContext", function (originalMethod) { return function (scope, callback) { return getDefaultContext(scope, callback) || originalMethod.call(this, scope, callback); } }); }
Finally, if you want the change to also apply to the “Complete Unscheduled Activities” dialog, you have two choices: you can either add a lookup control to the ScheduleCompleteActivity.ascx form, which is still a ASP.NET custom control with no customization hook, or override the activityService.completeNewActivity function. This is because by default, this function does not call the getActivityEntityContext method – makes sense, since those are normally provided from the form’s lookup. Adding the control to the ScheduleCompleteActivity form is straightforward but requires customizations that can’t be easily bundled. This is how you can instead apply the default context to the form:
aspect.around(ActivityService.prototype, "completeNewActivity", function (originalMethod) { return function (type, args) { var activityService = this; var showEditor = function (scope, ctx) { if (ctx) { lang.mixin(args, ctx); } originalMethod.call(activityService, type, args); } if (!getDefaultContext(activityService, showEditor)) showEditor(); } });
That’s it! Note that the example was pulled from an existing customization and I tried to remove all the custom fields from it but it’s possible I missed one so, if you get a problem with one of the sdata queries, make sure it’s not calling a field that’s not in your database.
I prepared an example bundle on github.