Workflow Comments Pipeline

Posted 02/03/2014 by Valerie Concepcion

Last week I had the pleasure of participating in the First Ever Sitecore Hackathon Contest as a member of Team Heisenberg (a.k.a., the ones who knock). Tasked with building the "Best Authoring Experience Enhancement Module", our team chose to develop a collection of related modules which enhance workflow functionality in Sitecore.

One of the ideas we had was to allow content authors to easily provide a link to the URL of a bug tracked in an external issue tracking system, such as JIRA. Typically, such systems provide direct links to issues via URLs that contain an issue ID (e.g., https://nttdata.atlassian.net/browse/PXI-342). Given that, some basic requirements for this feature were as follows:

  • An author has the ability to enter the ID of an issue and it will dynamically be replaced with a link to the corresponding issue URL.
  • The format of the issue IDs (e.g., PXI-342) can be configured.
  • The format of the issue URL can be configured.

As I started to dig into the WorkboxHistory.xml control and all the goodness that my colleague Akshay has nicely documented the inner workings of, I started to bounce ideas off my teammates and wonder if there wasn't a way to customize the workflow comment without even touching the UI. After all, comments appear in a few places (Workbox, Review tab, RSS feeds) and it would be a bit tedious to have to customize each of those channels individually.

Equipped with my .NET Reflector, web.config file, and about 20 hours to kill, I learned that each Sitecore database definition contains a configurable WorkflowProvider.

<workflowProvider hint="defer" type="Sitecore.Workflows.Simple.WorkflowProvider, Sitecore.Kernel">
  <param desc="database">$(id)</param>
  <param desc="history store" ref="workflowHistoryStores/main" param1="$(id)" />
</workflowProvider>

As shown above, the WorkflowProvider in turn references a HistoryStore, configured as a child element of the <workflowHistoryStores> section.

<workflowHistoryStores>
  <main type="Sitecore.Data.$(database).$(database)HistoryStore, Sitecore.Kernel">
    <param connectionStringName="$(1)" />
  </main>
</workflowHistoryStores>

And it's the GetHistory method of a HistoryStore instance that uses a SQL query to pull an item's history (including comment text) from the WorkflowHistory table of the database. In this method, the data from each history row is stuffed into a WorkflowEvent model and retrieved from the Workbox and other UIs, typically through a workflow.GetHistory(item) call, where "workflow" is an object of a type that implements the IWorkflow interface.

Workflow Classes
Workflow classes

Now wired on coffee at 2am, and zealous to learn Sitecore's mastery of dependency injection, I took the following approach to implement our workflow comment enhancement:

  1. Subclass the SqlServerHistoryStore class and add a reference to my custom implementation in the workflowHistoryStores section of the web.config.
        public class SqlServerHistoryStore : Sitecore.Data.SqlServer.SqlServerHistoryStore
        {
            public SqlServerHistoryStore(SqlDataApi api)
                : base(api)
            {
            }
    
            public SqlServerHistoryStore(string connectionString)
                : base(connectionString)
            {
            }
        }
    
        <workflowHistoryStores>
          <!-- Custom HistoryStore implementation which uses "getWorkflowComments" pipeline -->
          <custom type="Heisenberg.SCExtensions.Data.$(database).$(database)HistoryStore, Heisenberg.SCExtensions">
            <param connectionStringName="$(1)"/>
          </custom>
        </workflowHistoryStores>
    
  2. Run a custom getWorkflowComment pipeline from the GetHistory method which takes the comment text as a pipeline argument and allows it to be transformed via processors (<getworkflowcomment> : WorkboxHistoryXmlControl :: <renderfield> : FieldRenderer).
    	public class SqlServerHistoryStore : Sitecore.Data.SqlServer.SqlServerHistoryStore
        {
            public SqlServerHistoryStore(SqlDataApi api)
                : base(api)
            {
            }
    
            public SqlServerHistoryStore(string connectionString)
                : base(connectionString)
            {
            }
    
            /// <summary>
            /// Runs SQL query to get workflow events from WorkflowHistory table
            /// </summary>
            /// <param name="item"></param>
            /// <returns></returns>
            public override WorkflowEvent[] GetHistory(Item item)
            {
                Assert.ArgumentNotNull(item, "item");
                List<WorkflowEvent> list = new List();
                string sql = " SELECT {0}OldState{1}, {0}NewState{1}, {0}Text{1}, {0}User{1}, {0}Date{1} FROM {0}WorkflowHistory{1} WHERE {0}ItemID{1} = {2}itemID{3} AND {0}Language{1} = {2}language{3} AND {0}Version{1} = {2}version{3} ORDER BY {0}Sequence{1}";
                object[] parameters = new object[] { "itemID", item.ID.ToGuid(), "language", item.Language.ToString(), "version", item.Version.ToInt32() };
                using (DataProviderReader reader = this._api.CreateReader(sql, parameters))
                {
                    while (reader.Read())
                    {
                        string oldState = this._api.GetString(0, reader);
                        string newState = this._api.GetString(1, reader);
                        string text = this._api.GetString(2, reader);
                        string user = this._api.GetString(3, reader);
                        DateTime dateTime = this._api.GetDateTime(4, reader);
    
                        // Pipeline to customize workflow comment output
                        GetWorkflowCommentArgs args = new GetWorkflowCommentArgs(text);
                        CorePipeline.Run("getWorkflowComment", args);
                        text = Sitecore.StringUtil.GetString(args.Result);
                        
                        list.Add(new WorkflowEvent(oldState, newState, text, user, dateTime));
                    }
                }
                return list.ToArray();
            }
    	}
    
  3. Swap out the "workflowHistoryStores/main" parameter setting on the workflow provider with "workflowHistoryStores/custom".
    	<databases>
    	  <database id="master">
    		<workflowProvider hint="defer" type="Sitecore.Workflows.Simple.WorkflowProvider, Sitecore.Kernel">
    		  <!-- Switch to custom HistoryStore implementation -->
    		  <param desc="history store" ref="workflowHistoryStores/main" param1="$(id)">
    			<patch:attribute name="ref">workflowHistoryStores/custom</patch:attribute>
    		  </param>
    		</workflowProvider>
    	  </database>
    	</databases>
    
  4. Write an AddIssueTrackingLinks processor class to find issue IDs within a comment and replace them with fully-functioning HTML links.
    	/// <summary>
        /// Finds Issue IDs within a workflow comment and links them to their
        /// corresponding issue tracking URL.
        /// </summary>
        public class AddIssueTrackingLinks
        {
            /// <summary>
            /// Regular expression used to locate Issue IDs within comments (case-insensitive)
            /// </summary>
            static Regex _issueIdPattern = new Regex(Settings.GetSetting("WorkflowIssueTracker.IssueIdPattern"), RegexOptions.IgnoreCase);
    
            /// <summary>
            /// Replaces any issue IDs in the comment with fully-formatted HTML links
            /// </summary>
            /// <param name="args"></param>
            public void Process(GetWorkflowCommentArgs args)
            {  
                Assert.ArgumentNotNull(args, "args");
                if (!string.IsNullOrEmpty(args.Result))
                {
                    args.Result = _issueIdPattern.Replace(args.Result, GetIssueTrackingLink);
                }
            }
    
            /// <summary>
            /// Returns the fully-formatted HTML link for an issue
            /// </summary>
            /// <param name="match"></param>
            /// <returns></returns>
            protected virtual string GetIssueTrackingLink(Match match)
            {
                string issueId = match.ToString();
                string url = string.Format(Settings.GetSetting("WorkflowIssueTracker.UrlFormat"), issueId);
                return string.Format("<a href=\"{0}\" target=\"_blank\">{1}</a>", url, issueId);
            }
        }
    
        <settings>
          <!-- ISSUE TRACKER ISSUE ID PATTERN
               Regular expression to find issue ID's within a workflow comment. Match is case-insensitive.
            -->
          <setting name="WorkflowIssueTracker.IssueIdPattern" value="PXI\-\d+"/>
          <!-- ISSUE TRACKER URL FORMAT
               If an issue ID is found within a workflow comment, a link to thw issue URL will be created
               using the following string format.
            -->
          <setting name="WorkflowIssueTracker.UrlFormat" value="https://nttdata.atlassian.net/browse/{0}"/>
        </settings>
    
  5. Add the processor to the getWorkflowComment pipeline.
          <!-- Pipeline to apply changes to workflow comments -->
          <getWorkflowComment>
            <processor type="Heisenberg.SCExtensions.Pipelines.GetWorkflowComment.AddIssueTrackingLinks, Heisenberg.SCExtensions" />
          </getWorkflowComment>
    

So while one could have just customized the GetHistory method of the SqlServerHistoryStore to include the code from our AddIssueTrackingLinks processor and called it a night...could one have then extended their solution with processors to...

  • Lookup Sitecore usernames and replace them with email links? - e.g., "Please work with @vconcepcion to get the main image on this page resized."
  • Replace any properly-formatted URL with a functioning link - e.g., "Please make sure this matches the latest version of the copy deck which has been uploaded to http://cl.ly/image/1N2p5D291A1J"
  • Insert emoticon GIFs (because what better way to express yourself than in a workflow comment)? - e.g., "Nice job with this writeup smile"

OK, so I've only implemented the issue tracking thing so far, but just some examples of the possibilities. Check out the source code and use your imagination.

Source code on GitHub

Share:

Archive

Syndication