Extending Project Online and on-premises with JavaScript Apps Part 1

This is the first part of a three part series on extending project online / on-prem using JSOM, see this post for an index of all three parts to this as I write them.

Part 1: Getting started creating the Holiday Sync App

Let’s begin with a user story:

I want to import holiday exceptions into my enterprise calendars

Our solution:

We’re going to create a SharePoint hosted App for Project Server to do the following:

  • List enterprise calendars in PWA
  • Get holiday data from an external web service
  • Allow us to select holidays to we want to import
  • Provide an import button to perform the import

Here’s a quick wireframe created in PowerPoint to demonstrate what we’re trying to achieve (picture worth a dozen design docs?):

mockup

 

Software Requirements

To get started you’ll need a few things:

  • Visual Studio 2012 (or higher)

I’m going to use VS2013 so you will need Visual Studio to work with the attached source files, but it would be possible to use the “Napa” Office 365 development tools or other tools for this.

  • Web Essentials for Visual Studio

Highly recommended but not required.

  • Development environment (either of these):
  1. Project Online subscription
  2. Local Project Server 2013 development server

Wherever you’re debugging, make sure to enable “sideloading” of apps on PWA via PowerShell.

And finally note that Project Online is not currently included in the Office365 developer subscription so you’ll need a Project Online tenant in that case, and I’d recommend creating a separate PWA instance for development as enabling side-loading changes the homepage and enables features which should not be enabled on Production sites.

Getting Started with our App in Visual Studio

The first step is to create our blank SharePoint app in Visual Studio:

pic01

Second, I’m using a Project Online instance for debugging, but wherever you use make sure to set SharePoint-hosted for the hosting type:

pic02

This will give you the vanilla SharePoint App template, setup for our SharePoint-hosted JavaScript app, great now let’s start adding what we need, first up libraries and add-ins.

Libraries and Add-in’s

For our app we will be adding the following:

  • Project Server JSOM library (ps.js)
  • jQuery – Used extensively throughout solution.
  • jQuery UI – Used for the date picker UI controls.
  • Slick Grid – Used for our results grid.

Plus a few miscellaneous plugins:

The main extra library used is the grid control (Slick Grid) where actually any grid would do here as our requirements are really simple (JS Grid maybe?), but I like Slick Grid as it’s free (MIT license) and it’s fast and super easy to setup and use.

So with that list in mind let’s update our Default.aspx file, insert the following below the sp.js script line:

<script type="text/javascript" src="/_layouts/15/ps.js"></script>
<script type="text/javascript" src="../Scripts/libs/jquery-ui-1.10.3.custom.min.js"></script>
<script type="text/javascript" src="../Scripts/libs/jquery.xml2json.js"></script>

<link type="text/css" href="../Content/jquery-ui-1.10.3.custom.css" rel="stylesheet" />
<link type="text/css" href="../Content/slick.grid.css" rel="stylesheet" />

<script type="text/javascript" src="../Scripts/libs/jquery.event.drag-2.2.js"></script>   
<script type="text/javascript" src="../Scripts/libs/slick.core.js"></script>
<script type="text/javascript" src="../Scripts/libs/slick.grid.js"></script>    
<script type="text/javascript" src="../Scripts/libs/slick.checkboxselectcolumn.js"></script>
<script type="text/javascript" src="../Scripts/libs/slick.rowselectionmodel.js"></script>
<script type="text/javascript" src="../Scripts/libs/slick.dataview.js"></script>

To download the files in the libs folder see the bottom of this article, or you could even download them directly from the links above.

HTML and Stylesheets

I’m not a front end developer so I’ll freely admit that the following two parts took me longer than the rest of this App to write! But looks are important so it’s time well spent I’d say.

HTML for our Single Page App

Add this to the ContentPlaceHolderID=”PlaceHolderMain” section of the Default.aspx file;

<div class="HalfPage">
	<div class="ContentPane">
		<h2>Enterprise Calendar to Update</h2>
		<select id="eCalendarSelect">
		</select>

		<h2>Select Country</h2>
		<select id="countrySelect">
		</select>

		<h2>Date Range</h2>
		<div class="DatePickers">
			<div class="HalfPage">
				<label>From:</label>
				<input type="text" id="fromDatePicker" />
			</div>

			<div class="HalfPage">
				<span class="RightHalf">
					<label>To:</label>
					<input type="text" id="toDatePicker" />                    
				</span>
			</div>
		</div>

		<h2>Filter</h2>
		<div id="filterInputs" class="FilterOpts">                
			<ul class='listColumns'>
				<li><input id='Recognized' type='checkbox' value='Recognized' checked='checked' />Recognized</li>
				<li><input id='NotRecognized' type='checkbox' value='NotRecognized' />Not Recognized</li>
			</ul>
		</div>
	</div>
</div>

<div class="HalfPage">
	<h2>Holidays to Import</h2>
	<div id="grid" class="grid"></div>
</div>

<div>
	<input type="button" id="importBtn" class="ImportButton" value="Import Selected" />
	<input type="button" id="getDataBtn" class="RetrieveButton" value="Get Holidays" />
</div>

So in our HTML, we need a two column page with our required select and input elements with labels to identify them and also I’ve included the required filter options based on our holiday web service which will come back to later. That should be dynamic like the other controls which will be created in the JavaScript but that suits our purposes for now. Finally we have Div place holder for our grid control and some action buttons.

Most of the base styling including headers, fonts and labels are borrowing the SharePoint default styles available so the intention is to have our app look like native SharePoint / Project functionality. For the grid and other classes used I have a bit of CSS to add to the SlickGrid Excel style css theme that is available for that library, but I have modified it a bit as you can see in the CSS.

CSS Style Sheet

Place the following in the App.css:

.HalfPage {
    width:50%;
    float:left;
}
.HalfPage h2 {
    clear: left;
    padding: 16px 0 4px;
}

.ContentPane {
    margin: 0 32px 16px 0;
}
.ContentPane select {
    width:100%;
}

.DatePickers label {    
    margin-right: 16px;
}
.DatePickers input {
    width: 45%; 
}
.RightHalf {
    float:right;
    text-align: right;
}

.ui-datepicker-trigger {
    vertical-align: middle;
    padding-left: 4px;
}

.FilterOpts input {
    margin: 0 8px;
}

.ImportButton {
    margin-top: 24px;
    float:right;
}

.RetrieveButton {
    margin-top: 24px;
    float:left;
    margin-left: 0px !important;
}

.listColumns {
    -moz-columns:  2;
    -webkit-columns:  2;
    columns:  2;
    list-style: none;
    padding-left:0px;
}

#grid {
  outline: 0;
  border: 1px solid lightgrey;
  height: 280px;
}

.slick-viewport {
    overflow-x: hidden !important;
}

.slick-header-column.ui-state-default {
    padding-top: 6px;
    height: 26px;
}

.slick-header-columns {
    border-bottom: 1px solid silver;    
}

.ui-state-default {
    border: none;
    background: none;
    font-family: "Segoe UI","Segoe",Tahoma,Helvetica,Arial,sans-serif;
    font-size: 13px;
}

.ui-widget {
    font-family: "Segoe UI","Segoe",Tahoma,Helvetica,Arial,sans-serif;
    font-size: 13px;
}

Before you comment on my ugly CSS, lets just move along! :)

Helper.js JavaScript helper script

I’ve separated out our App code into just two files to keep this simple, the Helper.js file includes a number of functions that do as the name suggests, things like browser compatibility shims are ideal here as we will load this file before our main JS file.

Summary of helper functions:

  • Date.prototype.toISOString() – Used to support IE8 as it doesn’t support the ISO date string format.
  • Helpers.urlToArray() – Used to convert our URL query strings into an array of key / values.
  • Helpers.setupGrid() – The first part of our Slick Grid code used to setup the grid and required plug-ins.
  • Helpers.bankHolidayFilter() – Our Slick Grid filter function.
  • Helpers.dateTimeFormatter() – Our grid date formatter, for this I have hard coded to US format, but I would suggest using a library like Moment.js to help do this properly for all regions.
  • Helpers.updateGridContents() – Used to update our grid once we have transformed our holiday data as required.

Create a new file in your solution called Helpers.js and add the following;

'use strict';

///
/// Helpers
///
// Shim for IE8 to support date formatter
if (!Date.prototype.toISOString) {
    (function () {

        function pad(number) {
            if (number < 10) {
                return '0' + number;
            }
            return number;
        }

        Date.prototype.toISOString = function () {
            return this.getUTCFullYear() +
              '-' + pad(this.getUTCMonth() + 1) +
              '-' + pad(this.getUTCDate()) +
              'T' + pad(this.getUTCHours()) +
              ':' + pad(this.getUTCMinutes()) +
              ':' + pad(this.getUTCSeconds()) +
              '.' + (this.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) +
              'Z';
        };

    }());
}

// Static helper class
var Helpers = function () { };
Helpers.urlToArray = function (url) {
    var request = {};
    var pairs = url.substring(url.indexOf('?') + 1).split('&');
    for (var i = 0; i < pairs.length; i++) {
        var pair = pairs[i].split('=');
        request[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
    }
    return request;
};

/// Grid helpers
// Setup our Grid
// Using https://github.com/mleibman/SlickGrid
Helpers.setupGrid = function (grid, options, columns, data) {
    var checkboxSelector = new Slick.CheckboxSelectColumn({
        cssClass: "slick-cell-checkboxsel"
    });
    columns.unshift(checkboxSelector.getColumnDefinition());

    grid = new Slick.Grid("#grid", data, columns, options);
    grid.setSelectionModel(new Slick.RowSelectionModel({ selectActiveRow: false }));
    grid.registerPlugin(checkboxSelector);
    data.setFilter(Helpers.bankHolidayFilter);

    return grid;
}

// Grid filter function
Helpers.bankHolidayFilter = function (item, args) {
    var inArray = $.grep(args.BankHoliday, function (value) { return value === item.BankHoliday; });

    if (inArray.length > 0) {
        return true;
    }
    return false;
};
Helpers.dateTimeFormatter = function (row, cell, value, columnDef, dataContext) {
    // Basic US date formatter with fallback to ISO 
    var dt = new Date(value);
    if (isNaN(dt))
        return value.split("T")[0];
    else
        return dt.format("MM/dd/yyyy");
};

// Update the grid contents
Helpers.updateGridContents = function (holidays, gridData, grid) {
    // Setup grid filters        
    gridData.setFilterArgs({
        BankHoliday: $("#filterInputs input:checked").map(function () { return this.value; }).get()
    });

    // First need to add a uniquie identifier to the data set (to support SlickGrid filtering)
    for (var i = 0; i < holidays.length; i++) {
        holidays[i].id = i;
    }

    // Now use the data
    gridData.setItems(holidays);

    // Redraw the grid
    gridData.refresh();
    grid.invalidateAllRows();
    grid.render();
    grid.resizeCanvas();
} 

Finally we have a remove duplicates function that I will go into a little more detail on as it relates to our JSOM import that we’ll be adding later, so add the following function to the end of the Helper.js file.

// Remove duplicate calendar exceptions
Helpers.removeDuplicates = function (selectedRows, data) {
    var exceptionsToImport = [];

    // Use jQuery grep function to get our selected enterprise calendar
    var entCalendar = $.grep(data.entCalendars, function (cal) {
        return cal.Id === data.calendarId;
    })[0];

    // Prepare the array of exceptions excluding duplicates
    for (var i = 0; i < selectedRows.length; i++) {
        // Get this selected row from the Grid
        var selectedHoliday = data.holidayData.getItem(selectedRows[i]);

        // Ensure that our exception date does not overlap with any existing exceptions
        var alreadyExist = $.grep(entCalendar.BaseCalendarExceptions.results, function (excep) {
            return excep.Finish <= selectedHoliday.Date && excep.Start >= selectedHoliday.Date;
        });

        if (alreadyExist.length === 0) {
            // Last check to make sure we don't add the same date twice 
            var newException = $.grep(exceptionsToImport, function (item) {
                return item.Date === selectedHoliday.Date;
            });

            if (newException.length === 0)
                exceptionsToImport.push(selectedHoliday);
        }
    }

    return exceptionsToImport;
}

From testing it was clear that the following preconditions in Project Server exist which affect our app;

  • No exceptions can overlap in a single base calendar.
  • All exceptions must have a unique name in a single base calendar.

So our remove duplicates function ensures the above by doing the following;

  1. First looping through our selected rows array.
  2. Getting the item in each row and getting a reference to our enterprise calendar.
  3. Comparing the Start and Finish dates of our exception to ensure they do not overlap with any existing BaseCalendarExceptions (note that we’ll be populating this data in Part 2 of this series) – I’m using the jQuery Grep function for this as it’s quite flexible for this kind of thing.
  4. Then finally add this exception to our output array, after a quick check to ensure that we haven’t already added it with another name.

Now that we have all of our helpers in add a reference to the Helper.js file in the Default.aspx pointing to wherever you created your file, for example:

<script type="text/javascript" src="../Scripts/Helpers.js"></script>

Note: Ensure that the above line is placed ABOVE the App.js script reference!

App.js Main JavaScript file outline

For now we only have our basic structure and functionality in here, specifically I’m using the constructor prototype pattern in JavaScript here to clearly define our namespace and structure our code accordingly, trust me it might look more complex compared with many examples out there, but once you start working with asynchronous event based functions you really need to know where your this is! (Not to mention keeping the global context clean)

///
/// Namespace & constructor
///
var HolidaySync = function () {
    // Instance data
    this.urlTokens = Helpers.urlToArray(location.href);
    this.data = new HolidaySync.DataModel();

    // Grid configuration
    this.grid = undefined;
    this.columns = [
        { id: "Name", name: "Name", field: "Descriptor" },
        { id: "Date", name: "Date", field: "Date", formatter: Helpers.dateTimeFormatter },
        { id: "BankHoliday", name: "BankHoliday", field: "BankHoliday" }
        ];
    this.options = {
        enableCellNavigation: true,
        syncColumnCellResize: true,
        forceFitColumns: true
    };
};

So our first few lines to add to App.js declare our namespace and some instance properties, most of which relates to our Slick Grid (options and columns etc). We also have an instance of our DataModel instantiated which is done using the following function;

///
/// Data Model
///
HolidaySync.DataModel = function () {
    this.entCalendars = {};
    this.countryCodes = {};

    this.calendarId = '';
    this.country = '';
    this.fromDate = '';
    this.toDate = '';

    // Grid data
    this.holidayData = new Slick.Data.DataView();
};

Now that we have our “Public properties” I have a main function at the top with just a few lines for the moment:

///
/// Main code block, begin once the DOM is loaded
///
$(document).ready(function () {
    // Instantiate our object
    var holidaySync = new HolidaySync();

    // Setup our Grid
    // Using https://github.com/mleibman/SlickGrid
    holidaySync.grid = Helpers.setupGrid(holidaySync.grid, holidaySync.options, holidaySync.columns, holidaySync.data.holidayData);

    // Setup our page controls and events
    holidaySync.setupPageControls();

});

In there we instantiate our namespace defined above, then start calling some helper functions to setup our grid (defined in Helpers.setupGrid()) and then to setup our page using the following function:

// Function to wire-up our UI
HolidaySync.prototype.setupPageControls = function () {

    // Create our jQuery UI Date picker controls
    $("#fromDatePicker").datepicker({
        showOn: "button",
        buttonImage: "/_layouts/15/images/calendar.gif",
        buttonImageOnly: true,
        onSelect: Function.createDelegate(this, function (dateText) {
            this.data.fromDate = dateText;
        })
    });

    $("#toDatePicker").datepicker({
        showOn: "button",
        buttonImage: "/_layouts/15/images/calendar.gif",
        buttonImageOnly: true,
        onSelect: Function.createDelegate(this, function (dateText) {
            this.data.toDate = dateText;
        })
    });

    // Setup our page event handlers
    $("#countrySelect").change(Function.createDelegate(this, function (event, data) {
        this.data.country = $("#countrySelect option:selected")[0].value;
    }));
    $("#eCalendarSelect").change(Function.createDelegate(this, function (event, data) {
        this.data.calendarId = $("#eCalendarSelect option:selected")[0].value;
    }));
};

This function is all jQuery and creates our date pickers and change events on our drop-down select boxes. Note the context here, ie this.data.*** as we are on our prototype these controls asynchronously update our DataModel in this instance (hence why this is not in the static helper class).

With the above added you should now be able to preview the app! Nothing much will happen yet, but it should look pretty complete visually and all of our controls should function:

pic03

 

Next up, part 2: Getting data into our Holiday Sync App

Now that we have our App structure, next we need to work with some data, so check back in the next day or so for Part 2 of this series.

 

Source Download / Repository

Download the complete source to the above code here:

HolidaySyncDemo1.zip
HolidaySyncDemo1.zip

 

Additionally for those of you who like to skip to the end of this series, you can browse or download the full source code for the completed app on the following GitHub repository:

https://github.com/martinlaukkanen/holidaysync

 

Share and Enjoy !

Shares

Exploring REST Endpoints couldn’t be easier!

One of the best takeaways from ProjConf 2014 in my opinion was this gem by Chris Givens from Architecting Connected Systems:

http://sprest.architectingconnectedsystems.com/ 

Basically if you are working with the CSOM, JSOM or REST client side APIs for Project Server and SharePoint (and if you were at ProjConf after all the great sessions you have no excuse NOT to be! ;]), then you’ve probably found yourself browsing the REST endpoints to find what you need. I do all the time, in fact I will be writing about doing so for my Holiday Sync write up to be posted soon.

Using SPREST though this is made super easy, you can browse UP or DOWN the hierarchy of objects to find what you need (e.g. BaseCalendarExceptions) and it generate the code you need to use it via REST, CSOM or JSOM!

Not only that it has a voting feature where you can submit to MS (via Chris) suggestions of what unexposed methods you want to see in the client side that are exposed in the server side! Sweet.

 

It’s currently in Alpha, so check the site and support it by clicking on ad’s or buying the app when it’s out!

Share and Enjoy !

Shares

Bulk Edit App for Project Server Experience

I’ve been busy the last couple of months working away at my first crack at a SharePoint App as some of my recent posts here probably show. (Next up; a desperately needed refresh for this site!) So for anyone thinking about doing the same I thought it worth sharing some of my experiences in a quick write up.

However firstly: Go download Bulk Edit from the SharePoint App store right now! (And write a review!)

JSOM / CSOM

The first thing to say is when compared to the PSI API of the past even despite the near complete lack of documentation on JSOM / CSOM I’m loving it! To be able to write code that actually doesn’t fail half the time, who would of thought it possible?! :)

SharePoint Market Place

It’s early days yet, but what I can comment on so far is actually getting on to the Market place which in the end was in fact easier than I expected.

It took a couple of days to get my Seller account approved which was actually the longest wait. After that my app was approved within 48 hours of the second submission, that’s after the first submission was rejected within a few hours due to one of the automated-checks (make sure to read the links below).

To be honest the hardest thing was getting the App submission page to accept my Office365 subscription as “paid”, basically it requires a fully paid subscription linked to your seller account before you can submit a SharePoint app. The catch is that Partner accounts and trial accounts among others are not automatically accepted.

I’m now the proud owner of three O386 subscriptions; TechNet, MSPartner, and PAID MSDN Developer, yep I got there eventually!

If you have problems linking you account (steps here) make sure you give it some time, in my case it took overnight before the ‘verified’ subscription was accepted after following the verification steps.

Read the following if you want to know more on this process:

Thoughts on the SharePoint App Model

I’ve spoken to many people and read many other peoples thoughts on the whole new app model, seems to be a topic that divides opinions quite nicely.

I have to say in summary I do see some serious potential in the new model, sure SharePoint hosted apps are very constrained and Provider / Auto hosted are going to be an administrative nightmare in any typical highly locked down enterprise environment. But even still separating the solutions from SharePoint is a game changer in my opinion, that would be me coming squarely from the OPS site of DevOps!. That combined with solid client side APIs makes me actually happy to change from the old ways and old APIs.

Share and Enjoy !

Shares

Updating Lookup Table values using JSOM

As follow-up to my Updating Project Server custom fields via JSOM post I figured that as I only covered part of the problem a second post was needed to cover lookup table values.

In fact working out how to update Project Server lookup table based custom fields was actually much harder than I expected, so hopefully this saves someone else out there all the time I wasted!

 

Overview

Updating lookup table based custom field values uses the same method as normal custom fields:

PS.DraftProject.setCustomFieldValue(FieldName, Value);

Firstly you need to follow the instructions from part 1 to get the InternalName to use for the FieldName parameter, however what is completely different when updating lookup values is the second Value parameter.

If you’ve used PSI (or even just Reporting) before you probably recall how each lookup table value has a UID which represents its value, so what we need to do for custom fields with lookup table values is actually the following;

  1. Identify the internal name of the lookup table entry for the table.
  2. Pass the name to setCustomFieldValue in an acceptable format.

So a quick example should look something like this:

  PS.DraftProject.setCustomFieldValue(
    'Custom_x005f_d2bb8d78a1f5e211940b00155d000a03',
    ['Entry_fea76e70a1f5e211940b00155d000a03']);

Note: For those skim-reading pay attention to the [] or read on!

 

Obtaining a Lookup Table Entry Name

In the previous post I wrote we used JSOM to create an array containing all of our custom field details, now for our lookup table values we need need to create a another array to store the Lookup Entry properties.

Note: Using CSOM this is actually really easy: http://msdn.microsoft.com/en-us/library/office/microsoft.projectserver.client.lookupentrycollection_di_pj14mref.aspx

However due to the asynchronous nature of JSOM it is a little more complex.

Here’s an example JavaScript method which builds on top of the previous post code:

var fieldName = 'Programme';
var lookupEntries = [];

function getCFComplete(response) {
    var cfEnumerator = customFields.getEnumerator();

    while (cfEnumerator.moveNext()) {
        var cf = cfEnumerator.get_current();

        // Is this our custom field with lookup table values?
        if (cf.get_name() === fieldName) {
            var lookupTable = cf.get_lookupTable();

            // 2nd async request - load the LookupTable data
            projContext.load(lookupTable);
            projContext.executeQueryAsync(function () {
                var ltEntries = lookupTable.get_entries();

                // 3rd async request - load the lookup table entries
                projContext.load(ltEntries);
                projContext.executeQueryAsync(function () {
                    var ltEnum = ltEntries.getEnumerator();

                    while (ltEnum.moveNext()) {
                        var ltEntry = ltEnum.get_current();

                        lookupEntries.push({
                            InternalName: ltEntry.get_internalName(),
                            Value: ltEntry.get_value(),
                            FullValue: ltEntry.get_fullValue(),
                            Description: ltEntry.get_description()
                        });
                    }

                    // Done, now do something with the values
                    var myJsonString = JSON.stringify(lookupEntries)
                }, getCFFailed);
            }, getCFFailed);
        }
    }
}

If your following that then basically what we’re doing is the following:

  1. First enumerate all custom fields to find the field we want (in this example a custom field named ‘Programme’).
  2. Now asynchronously load the LookupTable data for that custom field.
  3. And then asynchronously load the LookupEntry data for the LookupTable and enumerate though it saving the values into our Array lookupEntries.

To give you an idea of what this looks like for the purpose of this guide in the code I have JSON’ified the result and saved it to a variable called myJsonString, which in my case looks like this:

[{"InternalName":"Entry_fda76e70a1f5e211940b00155d000a03",
"Value":"IT BAU","FullValue":"IT BAU","Description":""},
{"InternalName":"Entry_fea76e70a1f5e211940b00155d000a03",
"Value":"New Products","FullValue":"New Products","Description":""},
{"InternalName":"Entry_ffa76e70a1f5e211940b00155d000a03",
"Value":"Efficiency","FullValue":"Efficiency","Description":""}]

Setting the Custom Field Value

Now that we have our entry names we can use them in the setCustomFieldValue method. However there is one last thing to trip you up; the format. Simply passing the value as a string (e.g. ‘Entry_fea76e70a1f5e211940b00155d000a03’) will not work, and give no error. MSDN doesn’t help much either other than specifying the parameter as type “Object”.

So after a bit of trial and error I have found that you must pass the entry name value(s) in an Array, clearly this is to support multi-value custom fields, but it would be nice if MSDN mentioned this. :)

So now if we extend the updateProject() method from part 1, we get something like this;

function updateProject() {
    var projectId = "9C585CC0-3FC0-4133-9F2A-1FB96587CF0D";
    var project = projects.getById(projectId);
    var draftProject = project.checkOut();
    var fieldName = "My Custom Field";
    // Update custom field
    var cfData = $.grep(customFieldData, function (val) {
        return val.Name === fieldName;
    });

    // New part - get the lookup entry
    var leData = $.grep(lookupEntries, function (val) {
        return val.Name === "Some new value";
    });

    // If this value is in our lookup entry list then use it
    if (leData.length > 0 && cfData.length > 0) {
        draftProject.setCustomFieldValue(cfData[0].InternalName, leData[0].InternalName);
    }
    // Else handle the non-lookup table value
    else if (cfData.length > 0) {
        draftProject.setCustomFieldValue(cfData[0].InternalName, "Some new value");
    }

    //Publish the change
    var publishJob = draftProject.publish(true);
    //Monitor the job
    projContext.waitForQueueAsync(publishJob, 30, function (response) {
        if (response !== 4) {
            // handle errors
        }
    }
}

(Please note that code is written to demonstrate this concept and actually doesn’t make much sense as we have hard coded the custom field name and then are doing an if / else on the lookup entries being found!)

That’s it you should now be able to update any custom field including multi-value lookup tables using JSOM (or similarly with CSOM).

 

Is there a better way?

I set out originally to do this using purely JSOM, however it quickly becomes obvious that enumerating each custom field and caching all of the properties is quite tedious.

Fortunately there is a much better way using the REST interface, for example try the following URL on your project server:

http://[changethistoyourserverurl]/pwa/_api/ProjectServer/CustomFields

- <entry>
  <id>http://project2013test/PWA/_api/ProjectServer/CustomFields
  ('d2bb8d78-a1f5-e211-940b-00155d000a03')</id>
  <category  term="PS.CustomField"
    scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
  <link rel="edit" href="ProjectServer/CustomFields('d2bb8d78-a1f5-e211-940b-00155d000a03')"/>
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/EntityType"
    type="application/atom+xml;type=entry" title="EntityType"
    href="ProjectServer/CustomFields('d2bb8d78-a1f5-e211-940b-00155d000a03')/EntityType" />
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/LookupEntries"
    type="application/atom+xml;type=feed" title="LookupEntries"
    href="ProjectServer/CustomFields('d2bb8d78-a1f5-e211-940b-00155d000a03')/LookupEntries" />
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/LookupTable"
    type="application/atom+xml;type=entry" title="LookupTable"
    href="ProjectServer/CustomFields('d2bb8d78-a1f5-e211-940b-00155d000a03')/LookupTable" />
  <title  />
  <updated>2013-08-23T03:58:20Z</updated>
- <author>
    <name  />
  </author>
- <content type="application/xml">
- <m:properties>
      <d:AppAlternateId m:type="Edm.Guid">d2bb8d78-a1f5-e211-940b-00155d000a03
      </d:AppAlternateId>
      <d:Description  />
      <d:FieldType m:type="Edm.Int32">21</d:FieldType>
      <d:Formula  m:null="true" />
      <d:Id m:type="Edm.Guid">d2bb8d78-a1f5-e211-940b-00155d000a03</d:Id>
      <d:InternalName>Custom_d2bb8d78a1f5e211940b00155d000a03</d:InternalName>
      <d:IsEditableInVisibility m:type="Edm.Boolean">false</d:IsEditableInVisibility>
      <d:IsMultilineText m:type="Edm.Boolean">false</d:IsMultilineText>
      <d:IsRequired m:type="Edm.Boolean">false</d:IsRequired>
      <d:IsWorkflowControlled m:type="Edm.Boolean">false</d:IsWorkflowControlled>
      <d:LookupAllowMultiSelect m:type="Edm.Boolean">false</d:LookupAllowMultiSelect>
      <d:LookupDefaultValue m:type="Edm.Guid">fea76e70-a1f5-e211-940b-00155d000a03
      </d:LookupDefaultValue>
      <d:Name>Programme</d:Name>
      <d:RollsDownToAssignments m:type="Edm.Boolean">false</d:RollsDownToAssignments>
      <d:RollupType m:type="Edm.Int32">11</d:RollupType>
  </m:properties>
  </content>
</entry>

Note, that’s not the ODATA URL, it’s; /_api/ProjectServer/ and not /_api/ProjectData/!

Browsing the REST interface directly is a great way of learning about CSOM / JSOM, and for this example, you can quickly see the related link at the top titled ‘LookupEntries’, browse that and this is what you get:

http://[changethistoyourserverurl]/pwa/_api/ProjectServer/CustomFields(‘d2bb8d78-a1f5-e211-940b-00155d000a03’)/LookupEntries

- <entry>
      <id>http://project2013test/PWA/_api/ProjectServer/LookupTables
       ('fca76e70-a1f5-e211-940b-00155d000a03')/Entries('fda76e70-a1f5-e211-940b-00155d000a03')
      </id>
      ...
      <title  />
      <updated>2013-08-23T05:08:14Z</updated>
    - <author>
      <name  />
      </author>
    - <content type="application/xml">
    - <m:properties>
          <d:AppAlternateId m:type="Edm.Guid">00000000-0000-0000-0000-000000000000
          </d:AppAlternateId>
          <d:Description  />
          <d:FullValue>IT BAU</d:FullValue>
          <d:Id m:type="Edm.Guid">fda76e70-a1f5-e211-940b-00155d000a03</d:Id>
          <d:InternalName>Entry_fda76e70a1f5e211940b00155d000a03</d:InternalName>
          <d:SortIndex m:type="Edm.Decimal">1.0000000000</d:SortIndex>
          <d:HasChildren m:type="Edm.Boolean">false</d:HasChildren>
        - <d:Mask m:type="PS.LookupMask">
          <d:Length m:type="Edm.Int32">0</d:Length>
          <d:MaskType m:type="Edm.Int32">3</d:MaskType>
          <d:Separator>.</d:Separator>
          </d:Mask>
          <d:Value>IT BAU</d:Value>
      </m:properties>
      </content>
  </entry>

I’ve cut all but the first Entry from the data but as you can see it’s all there, in a nice XML format ready for use.

(I’ll leave that code to you!)

Share and Enjoy !

Shares

Updating Project Server custom fields via JSOM

After a few busy weeks working on my first 100% JavaScript 2013 App (watch this space for more!!) I’ve come to realise that the MSDN documentation on JSOM and CSOM still is pretty sparse!

A couple of simple examples exist in the usual place (e.g. JSOM CreateProjects) but when you get to the details you’ll find a lot missing. For example updating Custom Fields; if you look at the MSDN page covering PS.DraftProject, the method you need (draftProject.setCustomFieldValue()) is not even listed! (UPDATE 7/08: It is covered here but with no detail; PS.Project.setCustomFieldValue)

 

JavaScript PS.DraftProject.setCustomFieldValue Method

Here’s the missing method definition that you’ll see when using PS.debug.js:

PS.DraftProject.setCustomFieldValue(FieldName, Value);

Hey wow, that simple hey? No, unfortunately the definition is a bit misleading; easy to assume that FieldName references the custom field name used else where like in the OData fields, but in fact this refers to the InternalName from the PS.CustomField object.

An example InternalName is: Custom_a1737ae3b4fce211940b00155d000a03

So first thing you need to do is get that name, it is just the field GUID prefixed with “Custom_”, but I like to do things more dynamically so I’ll use projContext.get_customFields(); to cache that information.

 

Example JavaScript update of Custom Field Value

Firstly lets get those InternalName values into an array for later use.

Cache the field details with a GetCustomFields Function

var projContext;
var customFields;
var customFieldData = [];


SP.SOD.executeOrDelayUntilScriptLoaded(GetCustomFields, "PS.js");


function GetCustomFields() {
    // Initialize the current client context and get the projects collection
    projContext = PS.ProjectContext.get_current();


    customFields = projContext.get_customFields();
    projContext.load(customFields);

    // Run the request on the server.
    projContext.executeQueryAsync(getCFComplete, getCFFailed);
}

function getCFComplete(response) {
    var cfEnumerator = customFields.getEnumerator();


    // Save the details of each CF for later
    while (cfEnumerator.moveNext()) {
        var cf = cfEnumerator.get_current();


        customFieldData.push({
            Id: cf.get_id(),
            Name: cf.get_name(),
            InternalName: cf.get_internalName()
        });
    }

    // Now update the project
    updateProject();
}

Note the last line there; updateProject() as this is all asynchronous you need to call the update only once you have the customFieldData array ready.

 

Update the Project Custom Field Function

function updateProject() {
    var projectId = "9C585CC0-3FC0-4133-9F2A-1FB96587CF0D";
    var project = projects.getById(projectId);
    var draftProject = project.checkOut();
    var fieldName = "My Custom Field";


    // Update custom field
    var cfData = $.grep(customFieldData, function (val) {
        return val.Name === fieldName;
    });

    if (cfData.length > 0) {
        draftProject.setCustomFieldValue(cfData[0].InternalName, "Some new value");
    }

    //Publish the change
    var publishJob = draftProject.publish(true);


    //Monitor the job
    projContext.waitForQueueAsync(publishJob, 30, function (response) {
        if (response !== 4) {
            // handle errors
        }
    }
}

This simple example assumes the FieldType is text, but you get the idea, also to work with Lookup Tables you’ll need to look at the cf.get_lookupEntries() values in the getCFComplete() function but hopefully the above will get you started.

Share and Enjoy !

Shares