Sunday 3 May 2015

Remove Custom Actions from the List Item Context Menu in SharePoint

Ehy! (that's "hey", spelt wrong).

Ever had a solution that (erroneously) creates additional custom actions on the list item context menu?

I had this problem this week. A Nintex Workflow that I'm importing (via PowerShell) into a site during a provisioning process does just this. Apparently this happens when you have a workflow with the "Enable workflow to start from the item menu" setting enabled, and then you export that workflow and import it into another site.

The issue becomes apparently after you re-import the workflow more than once. Each time the workflow is re-imported (e.g. in my case when a provisioning process updates a site), the a new version of the custom action is added to the lists context menu, but the old one is not removed.

And this is what you end up with...


That doesn't look very good, and what's worse, only one of those custom actions (menu items) works (the most recently added one).

To sort this out, you can use PowerShell or SharePoint Designer to remove the old custom actions. Since I'm working on a provisioning processing, I used PowerShell to remove the context menu items.

I wrote the PowerShell as a function I could call from another script in my provisioning process. The PowerShell uses the client object model (CSOM), so that it doesn't need to be run on the SharePoint server.

Here it is!

Param(
        [Parameter(Mandatory=$true, Position=1)] [Microsoft.SharePoint.Client.ClientContext] $ClientContext,  
        [Parameter(Mandatory=$true, Position=2)][String]$listName, 
        [Parameter(Mandatory=$true, Position=3)][String]$actionName, 
        [Parameter(Position=4)][switch]$leaveFirstInstance
    )
#Load the web and list 
$web = $ClientContext.Web
$lists = $web.Lists
$ClientContext.Load($web)
$ClientContext.Load($lists)
$ClientContext.ExecuteQuery()        
$list = $lists.GetByTitle($listName)
$ClientContext.Load($list)
$ClientContext.ExecuteQuery()  
if($list -eq $null){
    Write-host "Couldn't find the list." -F Red
    return
} 
#Load the custom actions on the list
$allCustomActions = $list.UserCustomActions       
$ClientContext.Load($allCustomActions)
$ClientContext.ExecuteQuery()
#Get a collection of the custom actions that are on the context menu (ECB), 
#and match the name of the custom action passed to the function
$customActions = $allCustomActions | ?{$_.Title -eq $actionName -and $_.Location -eq "EditControlBlock"}
$itemCount = $customActions.Count
$removeFromIndex  = $itemCount - 1
$baseIndex = 0  
#If the leaveFirstInstance parameter has been set, then set the base index to one
#This ensures we leave the first item (custom action) in the collection  
if($leaveFirstInstance){
    $baseIndex = 1
}    
Write-Host "Found $itemCount custom actions ($actionName) to process in list $listName"
if($removeFromIndex -ge $baseIndex){
    $countOfItemsRemoved = 0
    $itemsToRemove = @()
    #Loop through all the custom actions in the collection
    #and build an array of custom action id's (GUID's) to delete
    $customActions | %{
        if($([Array]::IndexOf($customActions, $_)) -le $removeFromIndex){
            #Write-host "Adding item $($_.Id) to the array of items to remove"
            $itemsToRemove += $_.Id
        }
        #Write-Host "$($_.Id) with Index: $([Array]::IndexOf($l.UserCustomActions, $_))"
    }
    #For each item in the list of GUID's, get the custom action from the list 
    #(using the GUID) and then call the DeleteObject() method.
    $itemsToRemove | %{
        #Because we're modifying the collection of custom ations in each loop, 
        #we need to reload the list each time (so that the collection of 
        #user actions is unmodified.
        $lists = $web.Lists
        $ClientContext.Load($web)
        $ClientContext.Load($lists)
        $ClientContext.ExecuteQuery()
        #After (re)loading the list/collection, get the custom action
        #and call the DeleteObject() method
        $list.UserCustomActions.GetById($_.Guid).DeleteObject()
        $countOfItemsRemoved++
    }
    Write-host "Of the $($itemsToRemove.Count) items flagged to process, and removed $countOfItemsRemoved of those items."
    $list.Update()
}

And you can call it from another script like this (in this example, a script located in the same directory):

#The URL to the site where the list exists
$url = "https://ican.tell.you.com"
#Create the client context
$ClientContext = new-object Microsoft.SharePoint.Client.ClientContext($Url)
$cc = new-object System.Net.CredentialCache
$uri = New-Object  System.Uri $url 
$cc.Add($uri, "NTLM",[System.Net.CredentialCache]::DefaultNetworkCredentials)
$ClientContext.Credentials = $cc
$ClientContext.AuthenticationMode = [Microsoft.SharePoint.Client.ClientAuthenticationMode]::Default
$ClientContext.RequestTimeout = "500000"
#Call the function to remove the custom action
.\Remove-CustomActionsFromECB.ps1 -ClientContext $ClientSiteContext -ListName "Service Requests" -ActionName "Submit Service Request to iClient" -LeaveFirstInstance:$false