SPServices Stories #1 – How to Start a Workflow on Multiple Items in a List

This entry is part 1 of 20 in the series SPServices Stories

Introduction

Given the fact that so many people are using SPServices these days, I wanted to start posting some of the great things that people are doing with it out there.

If you have a story about how you are using SPServices that you would like to tell, ping me using the contact form. Your story doesn’t even have to include code, though people love to see examples. I’m always interested in what impact using SPServices may have had on your development philosophy, time to market with solutions, hiring practices, really anything that you feel SPServices has enabled you to do.

We can remove any identifying details if you feel that need to do so, but I’d like these stories to show off what *you* have done, so it’s great if you can take credit. I reserve the right to do a little editing for clarity, but otherwise you can write your own story. I’m also happy to help.

The first guest post is from fellow SharePoint MVP, Alberto Diaz Martin, who lives in Spain. Thanks to Alberto for kicking this series off!

[important]You can also read this post in Spanish on Alberto’s blog SharePoint 2010. Iniciar un flujo de trabajo en múltiples elementos de una lista[/important]

How to Start a Workflow on Multiple Items on a List

In SharePoint 2010 we have the option to select multiple items on a list. When you select several items, the ribbon allows you to Delete Items and Send Alerts, but where is the Workflow command?

To start a workflow on a list item, you have to go through the Start workflow page and if the workflow has an initialization form, you also have to enter the parameters. Because of this, SharePoint doesn’t allow users to start a workflow on multiple items simultaneously. But why not do so when we have a parameter-less workflow?

I think this is a missing feature on SharePoint 2010 because we can certainly do it using the SharePoint API or Web Services without any problems. What can we do to provide this capability to our users?

First, we need to create a Ribbon command using a Custom Action and in this action we will have two options to try to start the workflow. The first one uses an application page by passing the selected items as parameters and uses the server API to start the process. The second, and more flexible and elegant option is using JavaScript and the SharePoint Web Services to start each workflow per item.

SPServices Workflow Ribbon Custom Action

SharePoint Web Services are a horrible way to talk with SharePoint [ed: I disagree, but everyone is entitled to their opinion!] because the Web Services use XML to get and put parameters and options, and it’s not easy working with XML in JavaScript.

SPServices to the Rescue!!

As you know, SPServices is a jQuery library which encapsulates SharePoint Web Services with jQuery to make it easy to call them. SPServices has a Workflow namespace with some powerful operations and we can use StartWorkflow to start an item workflow, even if it has parameters.

It is so easy to use, you only need the Item URL, the workflow template Id and, if required, the workflow parameters.

$().SPServices({
  operation: "StartWorkflow",
  item: currentItemURL,
  templateId: workflowGUID,
  workflowParameters: workflowParams,
  async: true,
  completefunc: function () {
    SP.UI.Notify.addNotification("Workflow process started on selected item.", false);
  }
});

To get the workflow template Id, we have another function called GetTemplatesForItem that returns all the associated workflows for an item. All we have to do is get all the templates and find our workflow by name.

$().SPServices({
  operation: "GetTemplatesForItem",
  item: itemURL,
  async: true,
  completefunc: function (xData, Status) {
    var currentItemURL = this.item;
    $(xData.responseXML).find("WorkflowTemplates > WorkflowTemplate").each(function (i, e) {
      if ($(this).attr("Name") == "Invoice Approve") {
        var guid = $(this).find("WorkflowTemplateIdSet").attr("TemplateId");
        if (guid != null) {
          workflowGUID = "{" + guid + "}";
          //in this point, we have our workflow Id and we have to call the starting method
        }
      }
    }
  }
})

Now, we have to traverse the selected items in the custom action method, and for each item call the SPServices StartWorkflow method. Something like this:

function StarSignWorkflow(listId) {

  RemoveAllStatus(true);
  waitDialog = SP.UI.ModalDialog.showWaitScreenWithNoClose('Starting approval workflow process on selected item','Please,wait until we finished this long operation.',76,400);

  //Get the selected items
  clientContext = new SP.ClientContext.get_current();
  var web = clientContext.get_web();
  var list = web.get_lists().getById(listId);
  var items = SP.ListOperation.Selection.getSelectedItems(ctx);
  totaSelItems = items.length;

  //Because only have items Id,we need to use Client Object Model to get EncodeAbsUrl.
  var query = new SP.CamlQuery();
  var queryString = '<View><Query><Where><In><FieldRef Name="ID"/><Values>';
  for (index in items) {
    var valueString = '<Value Type="Integer">' + items[index].id + '</Value>';
    queryString = queryString + valueString;
  }

  query.set_viewXml(queryString + '</Values></In></Where></Query></View>');
  this.collListItems = list.getItems(query);
  clientContext.load(collListItems,'Include(EncodedAbsUrl)');

  //In the success callback,we’ll have all the selected items with the absolute url.
  clientContext.executeQueryAsync(Function.createDelegate(this,this.onInitProcessSuccess),Function.createDelegate(this,this.onInitProcessFail));
}

function onInitProcessSuccess() {

  var listItemEnumerator = this.collListItems.getEnumerator();

  //If our workflow has default init param,we can provide it in this way to run workflow with default values.
  var workflowParams = "<Data><Approvers></Approvers><NotificationMessage></NotificationMessage>" +
      "<DurationforSerialTasks></DurationforSerialTasks><DurationUnits></DurationUnits>" +
      "<CC></CC><CancelonRejection></CancelonRejection><CancelonChange></CancelonChange>" +
   "<EnableContentApproval></EnableContentApproval></Data>";

  try {
    var counter = 1;
    var total = totaSelItems;

    //Traverse all the selected items
    while (listItemEnumerator.moveNext()) {
      var oListItem = listItemEnumerator.get_current();
      var itemURL = oListItem.get_item('EncodedAbsUrl');
      var workflowGUID = null;

      //Before start the workflow,we used GetTemplatesForItem to get Workflow Template Id.
      $().SPServices({
        operation: "GetTemplatesForItem",
        item: itemURL,
        async: true,
        completefunc: function (xData,Status) {
          var currentItemURL = this.item;
          $(xData.responseXML).find("WorkflowTemplates > WorkflowTemplate").each(function (i,e) {
            if ($(this).attr("Name") == "Invoice Approve") {
              var guid = $(this).find("WorkflowTemplateIdSet").attr("TemplateId");
              if (guid != null) {
                workflowGUID = "{" + guid + "}";
                $().SPServices({
                  operation: "StartWorkflow",
                  item: currentItemURL,
                  templateId: workflowGUID,
                  workflowParameters: workflowParams,
                  async: true,
                  completefunc: function () {
                    if (total == counter) {
                      if (waitDialog != null) {
                        waitDialog.close();
                      }
                      SP.UI.Notify.addNotification("Started workflow approved process for selected invoice.",false);
                      window.location.reload();
                    }
                    counter++;
                  }
                });
              }
            }
          });
        }
      });
    }
  }catch (e) {
    if (waitDialog != null) {
      waitDialog.close();
    }
    AddStatus("There is an exception. Error: " + e.message,"red");
  }
}

As you can see, you have an easy way to provide an easy way to start a process on multiple items at the same time. Thanks to SPServices, working with SharePoint client side is more flexible and easy.

AlbertoDiazMartinAlberto Diaz Martin
MVP SharePoint
adiazcan@hotmail.com
@adiazcan
http://geeks.ms/blogs/adiazmartin

SPServices Stories #2 – Charting List Data with HighCharts

This entry is part 2 of 20 in the series SPServices Stories

Introduction

This submission comes to us from an anonymous reader, who can’t publish the details under his (or her) name due to confidentiality issues. However, s/he has been able to generate some very useful charts with HighCharts using SharePoint list data as the underlying data sources.

Charting List Data with HighCharts

HighChartsIn the following code, the source list contains columns with Year (string), Month (1-12), and Value (number with decimals).

While HighCharts isn’t free, the licensing costs are quite reasonable. A similar approach would work with other charting engines out there which may be free.

The page has a Content Editor Web Part dropped into it with the Content Link pointing to a file containing the following:

<!-- jquery and spservices are in the masterpage here this is a CEWP noConflict is on-->

<script type="text/javascript"src="//code.highcharts.com/highcharts.js"></script>
<script type="text/javascript" src="//code.highcharts.com/modules/exporting.js"></script>
<script type="text/javascript">
function GetYearSeries(series, year)
{
  var gotOne = false;
  var seriesOptions;

  jQuery.each (series, function(index, dataItem) {
    if (dataItem.name === year)
    {
      seriesOptions = dataItem;
      gotOne=true;
    }
  });

  if (!gotOne)
  {
    seriesOptions = {
      name: year,
      data: [0,0,0,0,0,0,0,0,0,0,0,0]
    };

    series.push (seriesOptions);
  }

  return seriesOptions;

}

jQuery(function($) {

  var CamlQuery = "<Query><OrderBy><FieldRef Name='Year' /><FieldRef Name='Month' Ascending='False' /></OrderBy></Query>";

  $().SPServices({
    operation: "GetListItems",
    async: true,
    listName: "Sales",
    CAMLQuery: CamlQuery,
    CAMLViewFields: "<ViewFields><FieldRef Name='Year' /><FieldRef Name='Month' /><FieldRef Name='Value' /></ViewFields>",
    completefunc: GraphIt
  });

  function GraphIt(xmlResponse)
  {

    var options = {
      chart: {
        renderTo: 'container',
        type: 'column'
      },
      title: {
        text: 'Sales'
      },
      xAxis: {
        categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
      },
      yAxis: {
        title: {
          text: 'Value'
        }
      },
      series: []
    };

    $(xmlResponse.responseXML).SPFilterNode("z:row").each(function() {

      var seriesOptions = GetYearSeries(options.series, $(this).attr('ows_Year'));
      var month=parseInt($(this).attr('ows_Month'))-1;

      seriesOptions.data[month]=parseFloat($(this).attr('ows_Value'));
    });
    var chart = new Highcharts.Chart(options);

  }

});//docReady
</script>

<div id="container" style="min-width: 400px; height: 400px; margin: 0 auto"></div>

This generates a chart which looks something like this:

HighCharts Example

SPServices Stories #3 – AddWebPart Method of the WebPartPages Web Service

This entry is part 3 of 20 in the series SPServices Stories

Introduction

Eric Alexander (@ejaya2) should be no stranger to those of you who have visited NothingButSharePoint‘s Stump the Panel forums. If you’ve ever visited those forums, odds are very good that Eric has helped you out. He’s a stalwart member of the SharePoint support community.

Eric wrote a post on his blog about using SPServices to add Web Parts to existing pages that I thought was worth sharing as an SPServices Story.

AddWebPart Method of the WebPartPages Web Service

There are a lot of documentation black holes out there in the Sharepoint land and it seems that the WebPartPages web service is one of them.

In a project I’m currently working on, there is a need to automate a project creation process that is very manual to something more automated. Fortunately we have Nintex Workflow to handle situations like this.​ Part of this workflow is to provision a new subsite after the project is approved by the project manager. Nintex makes this easy with their LazyApproval feature and create site action.

What I needed to do was upon site creation from a template, add some web parts to the home page. This is where I was running into the huge documentation gap. I turned to one of my goto libraries, SPServices, to protoype the actual calls before porting it over to my workflow. Fortunately someone had tried to do this same thing in the past and provided a working example for adding a content editor web part. That is described here and worked no problem. My issue is I need to add list view web parts of document libraries and lists. I tried many things over the span of a couple days to tweak that to get my web parts onto the page. No dice.

Today I stumbled upon a post by Glyn Clough that filled that documentation black hole.

I need to use the List View Web Part markup like this for lists:

<?xml version="1.0" encoding="utf-8" ?>
<webParts>
 <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
  <metaData>
   <type name="Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
   <importErrorMessage>Cannot import this Web Part.</importErrorMessage>
  </metaData>
  <data>
   <properties>
    <property name="ListUrl" type="string">Lists/CustomList</property>
    <property name="ExportMode" type="exportmode">All</property>
   </properties>
  </data>
 </webPart>
</webParts>

and like this for document libraries:

<?xml version="1.0" encoding="utf-8" ?>
<webParts>
 <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
  <metaData>
   <type name="Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
   <importErrorMessage>Cannot import this Web Part.</importErrorMessage>
  </metaData>
  <data>
   <properties>
    <property name="ListUrl" type="string">Library</property>
    <property name="ExportMode" type="exportmode">All</property>
   </properties>
  </data>
 </webPart>
</webParts>

Once escaped and passed into my SPServices function I had web parts on my web part page.

SPServices Code

Eric’s post doesn’t have the SPServices call in his post, so I thought I’d provide an example here. This is built from the example from the documentation on the SPServices Codeplex site for AddWebPart. It adds an XLV Web Part showing the default view of my Tasks list to the home page of my root site and shows me an alert with the results.

var str = "&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;" +
"&lt;webParts&gt;" +
 "&lt;webPart xmlns=&quot;http://schemas.microsoft.com/WebPart/v3&quot;&gt;" +
  "&lt;metaData&gt;" +
   "&lt;type name=&quot;Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c&quot; /&gt;" +
   "&lt;importErrorMessage&gt;Cannot import this Web Part.&lt;/importErrorMessage&gt;" +
  "&lt;/metaData&gt;" +
  "&lt;data&gt;" +
   "&lt;properties&gt;" +
    "&lt;property name=&quot;ListUrl&quot; type=&quot;string&quot;&gt;Lists/Tasks&lt;/property&gt;" +
    "&lt;property name=&quot;ExportMode&quot; type=&quot;exportmode&quot;&gt;All&lt;/property&gt;" +
   "&lt;/properties&gt;" +
  "&lt;/data&gt;" +
 "&lt;/webPart&gt;" +
"&lt;/webParts&gt;";

$().SPServices({
  operation: "AddWebPart",
  webPartXml: str,
  pageUrl: "/SitePages/Home.aspx",
  storage: "Shared",
  async: true,
  completefunc: function (xData, Status) {
    alert("Status: " + Status + " xData: " + xData.responseText);
  }
});

SPServices Stories #4 – Using SPServices to Process Files from a Non-Microsoft Source

This entry is part 4 of 20 in the series SPServices Stories

Introduction

SPServices Stories #4 comes to us from Michael Broschat. Michael claims he’s just a “regular old contractor, nothing special”. He lives in the DC metro area (Northern Virginia, specifically), although he’s originally a West Coast boy.

Using SPServices to Process Files from a Non-Microsoft Source

Our system involves email messages sent to a SharePoint library by a non-Microsoft forms application. Although this particular application normally saves its results as XML files, it cannot _send_ those results anywhere (say, the SharePoint library). It can send the results as delimited strings, however, without field names.

I developed a scheme whereby I provide the field structure in an array (arFields) then match it with the field values derived from the delimited-string email. I then write same to a list.

So, three steps in a four-step process have email looking something like this:

smithxx@us.here*BP*11/01/2012*11/19/2012*18*1 - Sent to Supervisor

The final step contains much more information, for a total of 25 fields. The endpoint list includes all 25 fields, of course. Typically, a user entry will comprise four entries but accommodation must also be made for changes made while the four-step process operates. Once a step 4 has been received, any further attempts to augment this process for that user are ignored.

This JavaScript code (with SPServices, jQuery) runs via an enabling button on the list display page. But even before the button appears, the first routine has run. This routine looks in the library for any entries that have not yet been exported. If any, the button is enabled and its label includes the number of records to read and create in an associated list.

$(document).ready(function() {
  jQuery.support.cors = true;
  $().SPServices({
    operation:  "GetListItems",
    listName: "{D3795486-9926-424E-8F14-59BE5DB65BA8}",	//dev  GFSSEForm
    CAMLViewFields: "<ViewFields><FieldRef Name='LinkFilename'/><FieldRef Name='Exported' /></ViewFields>",
    completefunc: function (xData, Status) {
      $(xData.responseXML).SPFilterNode('z:row').each(function() {
        var thisFSObjType = parseInt($(this).attr("ows_FSObjType").split(";#")[1]);
        if(thisFSObjType === 0) {
          if(parseInt($(this).attr("ows_Exported"))===0) iRecordsToExport++;
        }  //if this is a file name
      });  //each()
      if(iRecordsToExport > 0) {
        $("#btnInitiate").html('There are ' + iRecordsToExport + ' records to export...');
        $("#btnInitiate").prop('disabled', false);
      } else {
        $("#btnInitiate").html('No records to export...'); //evidently, once we're off IE7
        $("#btnInitiate").prop('disabled', true);
      }
    }  //completefunc
  });  //SPServices
});  //document.ready

When the Records to Export button is launched, more or less the same code runs again, but this time I want to see only those email entries that have not already been dealt with (ie, Exported = false). This is done via CAML. I collect the library IDs of all items I’m writing in this batch (by the way, there is an interesting distinction between dealing with a batch and dealing with the set of records already written to the list), along with a couple other values, and create an array of IDs. The rest of the processing in this application is from that array.

return {
  //the only public method; handler for the web page button (enabled when at least one library item has not been exported)
  processRecord: function () {
    //this filter restricts the selection just to items that haven't been exported;
    var myQueryOptions = "<QueryOptions />";
    var myQuery = "<Query><OrderBy><FieldRef Name='Created_x0020_Date' /></OrderBy><Where><Eq><FieldRef Name='Exported' /><Value Type='Integer'>0</Value></Eq></Where></Query>";
    // now sorts by Created, ascending.
    $().SPServices({
      operation:  "GetListItems",
      async: false,
      listName: "{D3795486-9926-424E-8F14-59BE5DB65BA8}",	//dev  GFSSEForm
      CAMLViewFields: "<ViewFields><FieldRef Name='LinkFilename'/><FieldRef Name='Exported' /></ViewFields>",
      CAMLQuery: myQuery,
      CAMLQueryOptions: myQueryOptions,
      completefunc: function (xData, Status) {
        $(xData.responseXML).SPFilterNode('z:row').each(function() {
          //pick up some metadata from the SharePoint library
          //gather ows_ID='14', ows_FSObjType='14;#0', ows_Created_x0020_Date='14;#2012-11-20 06:55:13'
          var thisFSObjType = parseInt($(this).attr("ows_FSObjType").split(";#")[1]);
          idCurrent = $(this).attr("ows_ID");	//available globally
          dateCurrentCreated = $(this).attr("ows_Created_x0020_Date").split(";#")[1];	//available globally
          var formFilename = $(this).attr("ows_LinkFilename");
          if(thisFSObjType == 0) {
            arIDs.push([idCurrent, dateCurrentCreated, formFilename]);
          }  //if this is a file name
        });  //each(); arIDs is an array built from looking at all non-Exported items in the library
        //actually, you need to know the contents before you can decide whether there are any duplicate entries.
        //	ordering by Created, we're processing all entries in order of their entry into the library
      }  //completefunc
    });  //SPServices
    //here, do the Ajax call against the array of arrays; async:false is no longer used with $.ajax, deferred
    //	taking over that function; 2 Jan took the following routine out from completefunc; seems to save a stack level
    $.each(arIDs, function(index, value) {
      var promise = $.ajax({
        type:"GET",
        url:"GFSSEForm/" + arIDs[index][2],		//for SP2010 and above, full path is needed for the library name
        dataType:"text"
      });
      //the promise code executes when the file has been opened by AJAX
      promise.done(doStuff);	//magically passes the data along, too
      promise.fail(function() {alert("failed to open this eform: " + arIDs[index][2]);});
    });  //each
  }
};	// public method processRecord() [return]

By this point, the email file in the SharePoint library has been opened and its contents are ready to process.

There are two ways for email to get into the library: sent by the non-Microsoft forms application and sent directly via a client’s Outlook. The former way gets encoded (binary64), whereas the latter way does not. Both must be parsed for usable content but the encoded email must first be decoded. I use the following routine to handle both:

//	Sets the global variable: arValues; deals with both base64-encoded email and also non-encoded email
//	CRLF = \r\n
function decodeEmail(block) {
  var iBase64 = block.indexOf("base64");
  if(iBase64 > -1) {
    var startBlock = iBase64 + 6;
    var endBlock = block.indexOf("\n--", startBlock);
    var emailBlock = $.trim(block.substring(startBlock, endBlock));
    var strEmail = emailBlock.replace(new RegExp("\\n", "g"), "");
    var strDecoded = Base64.decode(strEmail);
    strDecoded = stripHTML(strDecoded);
    var iLong = strDecoded.indexOf("\r\n\r\n");	//intended for non-SMTP messages
    if(iLong > -1) {
      //take up to first \r\n
      strDecoded = strDecoded.substring(0,iLong+2);
    }
    arValues = strDecoded.split("*");
  } else {
    //	charset="us-ascii"; charset=utf-8; charset="gb2312"
    //here if there was no "base64" in the message; perhaps you should look for [charset="]us-ascii["]
    // this routine greatly strengthened 24 Jan 2012
    var iTrueStart = block.indexOf("quoted-printable");	//24 Jan fine; whole routine looks good
    var iTrueStart2 = iTrueStart + 16;
    var endBlock = block.indexOf("\n--", iTrueStart);
    var strBlock2 = $.trim(block.substring(iTrueStart2, endBlock));
    var newBlock = strBlock2.replace("=\r\n", "");		//kill all CRLFs
    var newBlock2 = newBlock.replace(/\<.*\>/g, "");  //a weird <mailto...> string in one message
    //you could have just called your own stripHTML()!
    var newBlock3 = newBlock2.replace(/=\r\n/g, "");	//one last holdout: =CRLF
    arValues = newBlock3.split("*");

  }
}

In my experience, getting values from functions does not always work within this environment (JavaScript within SharePoint). I have had to rely upon global variables (which have created their own problems at times). When that email decoding code runs, it places the parsed values into a global array: arValues, which is then used by the various routines that follow.

doStuff runs when the promise has been fulfilled. In other words, it only runs when data from the email is in hand. It sends the record off to writeNew or, if the number of field values does not match the current field template, stops the record from being processed.

function doStuff(data) {
  decodeEmail(data);	//sets global arValues, regardless of email type
  arFields = [];	//to ensure correct value within the batch loop
  if(arValues.length == 7) arFields = arFields100;
  if(arValues.length == 25) arFields = arFields200;
  if(arFields.length > 0) {
    boolIgnore = false;	//ensures correct starting point for each email
    iGlobal++;
    arValues[2] = dateFormat(arValues[2]);	//watch for changes in this default field order
    arValues[3] = dateFormat(arValues[3]);
    if(arValues.length === arFields.length) {
      var strArguments = arValues.join();
      writeNew(strArguments);	//wait until this routine before handling a dupe;
    } else alert("Number of values differed from number of form fields; not written." + arFields.length + " fields, " + arValues.length + " values (" + arValues + ")");
  } // was arFields set? If not, just return and let it pass
}  // doStuff()

writeNew simply uses SPServices to write the email contents to the list. It does this by preparing an array value to contain the values in the proper manner for UpdateListItems::New. After writing the record, I call setUpdated to modify the library entry, passing the library ID, which setUpdated uses to access the library metadata.

// strArguments is arValues rendered as string
function writeNew(strArguments) {
  $("#divId").ajaxError( function(event, request, settings, exception) {
    $(this).append("Error here: " + settings.url + ", exception: " + exception);
  });
  var iFields = arFields.length;
  var i = 0;
  var strTest = "";
  var strField = "";
  var vpairs = [];
  var strPairs = "";
  var arValues2 = strArguments.split(',');
  for(i=0; i<iFields; i++) {
    strTest = String(arValues2[i]);
    if(strTest.length > 255) strTest = strTest.substring(0,255);
    strField = arFields[i];
    vpairs.push([strField,strTest]);
  }
  //check to see whether this email address is in HoldMe; if so, processing stops, but run setUpdated(arIDs[idIndex][0]) and advance the index
  notInHoldMe(vpairs[0][1]);	//sets global value regarding presence in HoldMe list
  if(!inHoldMe) {
    var jsDate = getJSDate();		//picks up date values from arValues
    vpairs[4][1]=jsDate;
    $().SPServices({
      operation: "UpdateListItems",
      batchCmd: "New",
      async: false,
      listName: "{2D9F4CDB-A5F0-4EED-8996-C26FB2D08294}",  //development list GFSSVerified
      valuepairs: vpairs,
      completefunc: function(xData, Status) {
        if(Status == 'success') {
          //'success' is a relative term; you must also examine any error text, to see whether an error occurred
          var strError = $(xData.responseXML).SPFilterNode('ErrorText').text();
          if(strError != "") {
            $("#showErrors").append("<p>Error adding: " + $(xData.responseXML).SPFilterNode('z:row').attr("ows_Title") + " " + strError + "</p>");
          } else setUpdated(arIDs[idIndex][0]);  //possibly delete the row at this point
          idIndex++;
          if(vpairs[6][1].substring(0,1) == "4") setLocked(vpairs[0][1]);	// ie, after writing Step 4
          //record has been written; now find out whether it was a duplicate
          else findExisting(vpairs[0][1], vpairs[6][1].substring(0,1));
        } else alert("error: " + xData.responseText);
      }	//completefunc
    }); //SPServices
  }	//if not locked
  else {
    alert("The record for " + vpairs[0][1] + " is locked...");
    setUpdated(arIDs[idIndex][0]);
    idIndex++;
  }
}  // writeNew()

All records are to be written to the list, but it will happen that some records are duplicates (because a later action changes the previous action). In that case, the earlier record needs to be marked as ‘orphan’. The original idea was to simply delete the record but someone wanted to keep it. Therefore, I need to filter orphans from various stages of processing. The routine called findExisting deals with this issue. I use CAML to filter orphans.

function findExisting(user, action) {
  $("#divId").ajaxError( function(event, request, settings, exception) {
    $(this).append("Error in findExisting: " + settings.url + ", exception: " + exception + "<br>");
  });
  var queryOptions = "<QueryOptions />";
  var query = "<Query><Where><And><And><Eq><FieldRef Name='Title' /><Value Type='Text'>" + user + "</Value></Eq><BeginsWith><FieldRef Name='col07x' /><Value Type='Text'>" + action + "</Value></BeginsWith></And><Neq><FieldRef Name='col26x' /><Value Type='Integer'>1</Value></Neq></And></Where></Query>";  //col26x is 'Orphan'
  //CAML looks for existing items having same name and action, ignoring any that have already been marked as orphans
  $().SPServices({
    operation:  "GetListItems",
    async: false,		//required!!!!!
    listName: "{2D9F4CDB-A5F0-4EED-8996-C26FB2D08294}",  //development list GFSSVerified
    CAMLViewFields: "<ViewFields><FieldRef Name='Title'/></ViewFields>",
    CAMLQuery: query,
    CAMLQueryOptions: queryOptions,
    completefunc: function (xData, Status) {
      var iCount = parseInt($(xData.responseXML).SPFilterNode("rs:data").attr("ItemCount"));
      if(iCount > 1) {	//you're here because this value _at least_ was written
        //within this batch, there are multiples of this user/action; pass the multiple IDs
        var arDupIDs = [];
        var iDupID = 0;
        // routine examines each entry in arDupIDs, and replaces any value with lesser; ends up with earliest entry, which is then orphaned
        // limitation here is that it only--practically speaking--handles two instances; three or more would lose all but one
        $(xData.responseXML).SPFilterNode('z:row').each(function() {
          iDupID = parseInt($(this).attr("ows_ID"));
          arDupIDs.push(iDupID);
          if(arDupIDs.length > 1) {
            if(iDupID < arDupIDs[0]) arDupIDs[0] = iDupID;
          }
        });
        orphanGFSSRow(arDupIDs[0]);
      }	// if at least one
    }	//completefunc
  });  //SPServices
}	//findExisting()

The completefunc routine looks only at items that have duplicates. It determines the earliest item, then sends off its ID for marking as orphan.

SPServices is also used to lock an account (by placing the email address in a separate list), by checking for existence of the email address currently being processed in the Locked list. One function sets the lock, while notInHoldMe() queries the lock list.

SPServices Stories #5 – Gritter and Sharepoint: Integrate Nifty Notifications in Your Intranet!

This entry is part 5 of 20 in the series SPServices Stories

Introduction

This SPServices Story comes to use from Rick El-Darwish (@RtfulDodg3r) in Geneva, Switzerland. For those of you who aren’t familiar with it, Gritter is a Growl-like notification jQuery plugin. That’s a mouthful, but try the links and you’ll quickly get the picture. If you’re an OS X user, then you’ll recognize the capability right away.

Proof of concept Gritter SharepointProof of concept Gritter Sharepoint hover

Sure, SharePoint has a notification scheme built into it – Showing transient messages using the SharePoint 2010 Notification Area – SharePoint 2010 UI tip #2 (thanks, as always, @waldekm) – and there is at least one Codeplex project – SPNotifications - that provides similar functionality. However, the built-in notifications are limited in functionality and the SPNotifications requires server-side code. The script-based approach has a much smaller footprint and the content can easily be maintained by an end user. (Getting developers out of the mix with content ought to always be the goal.) Also, by using open source jQuery-based alternatives, you always have the option to expand the functionality to meet your own needs.

By using SPServices and Gritter together, Rick was able to create very user-friendly notifications on SharePoint pages which were driven by list-based content. Rick starts out with a simple example and shows you how to set up the list-driven approach. With a little extrapolation, I’m sure you will be able to see quite a few additional uses for this approach in your own environments.

You can see Rick’s original post on his blog, Forensic Aspirations, Inferred Logic: Rick’s tale of FAIL.

Gritter and Sharepoint: Integrate Nifty Notifications in Your Intranet!

I’m working with a client on building up a project management tool in SharePoint; one thing that’s really piqued their interest is the concept of getting short flashes of information when their staff logs into the landing page. I think it’s a lovely idea, and I’m intend on giving them this functionality – but how, you might ask? There doesn’t seem to be any SharePoint feature for doing that!

We tend to forget that SharePoint is, above all, a web-based solution. This means that, with a little ingenuity (and sometimes a lot of sweat and blood), you can integrate some of the latest, coolest web features into your existing SharePoint. Fortunately, notifications are not too complicated. In this short article, we’re going to walk through creating very cool notifications using Gritter, a jQuery-based “plugin”, with Sharepoint.

Step One: Create a Sandbox

This may be as simple as creating a new page in your Site Pages repository. I seriously recommend implementing a proof-of-concept rather than work on your production page… If you’re not familiar with these libraries, the last place you want to test things out is on your production work, as easy as these implementations may seem.

Apart from your page, your sandbox will need a few extra files. These you can either place in the Site Assets repository of your PoC portal, or in the Site Assets repo of your root. The latter has the benefit of being accessible to your entire audience (or at least I assume so, it will depend on your permissions). The files that you need are the latest minified version of jQuery, the latest version of Gritter, and the latest version of SPServices (double-check these pages for compatibility issues, of course – if Gritter or SPServices tell you that they won’t work with certain versions of jQuery, don’t use those versions…)

When downloading Gritter, you’ll notice that it is a zip file that has several folders and files. I recommend that you keep those in one single place in your Site Assets. I find it’s easiest to use SharePoint Designer to do that.

Step Two: Add Your jQuery References

Now that you have a sandbox, you can start working with it. In case you’re wondering, this section is assuming that you’re working with SharePoint Designer (SPD) to do your work.

Open your sandbox page in SPD, editing it in Advanced Mode. Locate the PlaceHolderMain placeholder and add the references to your script files:

<!– Inserted by Rick – references to jQuery scripts –>
<!– These are the references to jQuery and SPServices. –>
<script type="text/javascript" src="../SiteAssets/jquery-1.8.3.min.js"></script>
<script type="text/javascript" src="../SiteAssets/jquery.SPServices-0.7.2.min.js"></script>
<!– These are the references to Gritter: –>
<link rel="stylesheet" type="text/css" href="../SiteAssets/gritter/css/jquery.gritter.css" />
<script type="text/javascript" src="../SiteAssets/gritter/js/jquery.gritter.min.js"></script>
<!– End Rick insertions. –>

You can test that the libraries loaded correctly by firing up Chrome, navigating to your PoC page, and opening your Console (F12). In the Console, type the following:

$
$(document).SPServices
$.gritter

If any of these return ‘undefined’, review your references, make sure the files are uploaded in the correct location.

Step Three: Setting up your notifications!

OK, now we know that all the necessary libraries are loaded. Time to develop notifications. Always develop incrementally, testing your code one chunk at a time. To that effect, here’s code that you should insert after the above script blocks:

<!– Here’s a test notification –>
<script type="text/javascript">
$(document).ready( function() {
  $.gritter.add({
    title: "This is a test notification",
    text: "This will fade after some time"
  });
});
</script>

If that works, you know that Gritter is functional for static content! Now it’s time to pull the real notifications from a list — this is where SPServices comes in. Before we proceed, we need something to pull information from: create a custom list with a single title for your PoC, “Latest Activities” for instance. Then, you will call the GetListItems function using SPServices.

The following code replaces your test notification:

<!– Code for notifications –>
<script type="text/javascript">

//This function throws up the notification:
function notify(curTitle, curContent) {
  $.gritter.add({
    title: curTitle,
    text: curContent
  });
}

//This retrieves the latest item of your Latest Activities.
function getLastActivity() {
  $(document).SPServices({
    operation: "GetListItems",
    async: false,
    listName: "Latest Activities",
    CAMLRowLimit: 1,
    CAMLQuery: "<Query><OrderBy><FieldRef Name=’Created’ Ascending=’False’ /></OrderBy></Query>",
    CAMLViewFields: "<ViewFields><FieldRef Name=’Title’ /></ViewFields>",
    completefunc: function(xData, Status) {
      $(xData.responseXML).SPFilterNode("z:row").each(function() {
        notify(‘For your information…’, $(this).attr("ows_Title"));
      });
    }
  });
}

//On document load, throw up the notification:
$(document).ready( function() {
  getLastActivity();
});
</script>

Et voilà — Gritter and SharePoint notifications in a nutshell! Your page will load and, once loaded, will call the getLastActivity function. getLastActivity pulls the latest item from the Latest Activities list (we use the CAMLQuery parameter to order by create date, and the CAMLRowLimit parameter to only return one parameter), and use a callback function to call the notify() function. The notify function is what is responsible for rendering the Gritter notification.

Happy notifying!

Rick.