Bookmarklet: Display Optionset values

Unless you have generated strongly typed classes, looking up Optionset values for the selected value or any other value, is one of the most annoying things to do as a developer. You can use the bookmarklet below to display Optionset values, along with the Optionset text for all the Optionsets on a form. You can drag the minified source to your bookmark bar, for creating the bookmarklet.

This is how a sample contact form looks after executing the bookmarklet.

Altered Optionset

Unminified

(function () {
	var contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		var Xrm = contentPanels[0].contentWindow.Xrm;
		var frameDocument = contentPanels[0].contentWindow.document;
		Xrm.Page.ui.controls.forEach(function (c) {
			if (c.getControlType() !== 'optionset')
				return;
			var attribute = c.getAttribute();
			var selectedOptionValue = attribute.getValue();
			var options = attribute.getOptions();
			var isClearOptions = options.some(function (o) {
					return o.text.indexOf(' (') === -1;
				});
			if (isClearOptions) {
				c.clearOptions();
			}
			options.forEach(function (o) {
				if (o.text && o.text.indexOf(' (') === -1) {
					o.text = o.text + ' (' + o.value + ')';
				}
				c.addOption(o);
			});
			if (selectedOptionValue && isClearOptions) {
				attribute.setValue(selectedOptionValue);
			}
		});
	} else {
		alert('Entity form not detected');
	}
})();

Minified

javascript:(function(){var contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){var Xrm=contentPanels[0].contentWindow.Xrm;var frameDocument=contentPanels[0].contentWindow.document;Xrm.Page.ui.controls.forEach(function(c){if(c.getControlType()!=='optionset')
return;var attribute=c.getAttribute();var selectedOptionValue=attribute.getValue();var options=attribute.getOptions();var isClearOptions=options.some(function(o){return o.text.indexOf(' (')===-1;});if(isClearOptions){c.clearOptions();}
options.forEach(function(o){if(o.text&&o.text.indexOf(' (')===-1){o.text=o.text+' ('+o.value+')';}
c.addOption(o);});if(selectedOptionValue&&isClearOptions){attribute.setValue(selectedOptionValue);}});}else{alert('Entity form not detected');}})();void 0;
Advertisements

Bookmarklet: Customise Form & Refresh Subgrids

If you just want to quickly customise the form layout use this bookmarklet below. Don’t add any new fields from this window, as it will create the field with the Default Publisher’s prefix, which by default is “new_”.

Unminified

var entityName = prompt("Entity?", "");
var url = Xrm.Page.context.getClientUrl() + '/main.aspx?etn=' + entityName + '&extraqs=formtype%3dmain&pagetype=formeditor';
window.open(url, '_blank');

Minified

javascript:var%20entityName=prompt("Entity?","");var%20url=Xrm.Page.context.getClientUrl()+'/main.aspx?etn='+entityName+'&extraqs=formtype%3dmain&pagetype=formeditor';window.open(url,'_blank');void%200;

If you just want to quickly refresh all the subgrids in the form, without refreshing the whole page, use this bookmarklet.

Unminified

(function () {
	var contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		var Xrm = contentPanels[0].contentWindow.Xrm;
		Xrm.Page.ui.controls.forEach(function (c) {
			if (c.getControlType() === 'subgrid') {
				c.refresh();
			}
		});
	} else {
		alert('Entity form not detected');
	}
})();

Minified

javascript:(function(){var%20contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return%20d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){var%20Xrm=contentPanels[0].contentWindow.Xrm;Xrm.Page.ui.controls.forEach(function(c){if(c.getControlType()==='subgrid'){c.refresh();}});}else{alert('Entity%20form%20not%20detected');}})();void%200;

Advanced Find and “In” condition

Advanced Find, in its current form, doesn’t have the capability to do a true “In” condition for text, datetime and numbers. For example this is how you would really do an “In” condition for a text field.

In condition

However the above condition would not return any results, as Advanced Find doesn’t allow separating valid values with a semi-colon. Query like the one above, has to be actually be done like the screenshot below.

In condition Actual

This is actually clunky and involves too many clicks. The query capability in Advanced Find also differs to what the workflow condition offers. For e.g. you can do a true “In” condition in workflow editor. The workflow condition builder uses a similar UI to Advanced Find, but it also displays a “In” condition for text attribute, which is not displayed in the Advanced Find.

Workflow In Condition

Workflow Step

Bookmarklet Solution

In order to do a “in” condition, where the target values are separated by semi-colons, you can use the bookmarklet below.

Unminified code

var contentPanel = $('#crmContentPanel > iframe');
if (contentPanel && contentPanel.length > 0) {
	var targetFrame = contentPanel[0].contentWindow;
	var isFetchModified = false;
	targetFrame.ExecuteQuery();
	var xml = targetFrame.$get("FetchXml").value;
	var xmlDoc = $.parseXML(xml),
	$xml = $(xmlDoc);
	$xml.find('condition[value]').each(function (i, d) {
		var inCondition = '';
		var multipleValues = $(d).attr('value');
		if (multipleValues.indexOf(';') === -1)
			return;
		multipleValues.split(';').forEach(function (c) {
			inCondition += ('<value>' + c + '</value>');
		});
		$(d).attr('operator', 'in');
		$(d).removeAttr('value');
		$(d).html(inCondition);
		isFetchModified = true;
	});
	if (isFetchModified) {
		targetFrame.$get("FetchXml", $get("resultRender")).value = '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true">' + xmlDoc.documentElement.innerHTML + '</fetch>';
	}
	targetFrame.changeArea(targetFrame.ResultsPage);
	targetFrame.$get('resultRender').submit();
} else {
	alert('Cannot locate Advanced Find Frame');
}
void 0;

Minified code for bookmark bar

javascript:var contentPanel=$('#crmContentPanel > iframe');if(contentPanel&&contentPanel.length>0){var targetFrame=contentPanel[0].contentWindow;var isFetchModified=false;targetFrame.ExecuteQuery();var xml=targetFrame.$get("FetchXml").value;var xmlDoc=$.parseXML(xml),$xml=$(xmlDoc);$xml.find('condition[value]').each(function(i,d){var inCondition='';var multipleValues=$(d).attr('value');if(multipleValues.indexOf(';')===-1)
return;multipleValues.split(';').forEach(function(c){inCondition+=('<value>'+c+'</value>');});$(d).attr('operator','in');$(d).removeAttr('value');$(d).html(inCondition);isFetchModified=true;});if(isFetchModified){targetFrame.$get("FetchXml",$get("resultRender")).value='<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true">'+xmlDoc.documentElement.innerHTML+'</fetch>';}
targetFrame.changeArea(targetFrame.ResultsPage);targetFrame.$get('resultRender').submit();}else{alert('Cannot locate Advanced Find Frame');}
void 0;

You basically continue to use the “equal” condition and then separate the valid values with semi-colon. Run the bookmarklet after that and the “equal” condition will be converted into a “in”. This currently works only for text fields. Datetime and number fields have additional format validations, hence it is not possible to use “;” to separate out the values.

I have tested this bookmarklet in CRM Online and CRM2015 with Firefox and Chrome.

Known Issues

A javascript error pops up before the results are displayed in Firefox. This can be ignored as the correct results are shown.

Bookmarklet: Turn off autosave & lookup on newwindow

The first bookmarklet turns off autosave for the current record, and refreshes the form without saving. Drag the below to your favorites bar.

javascript:(function(){var%20contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return%20d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){var%20Xrm=contentPanels[0].contentWindow.Xrm;Xrm.Page.data.refresh(false).then(function(){Xrm.Page.data.entity.addOnSave(function(econtext){var%20eventArgs=econtext.getEventArgs();if(eventArgs.getSaveMode()===70||eventArgs.getSaveMode()===2){eventArgs.preventDefault();}});alert('Form%20refreshed%20without%20save.%20Autosave%20turned%20off.');},function(errorCode,message){alert(message);});}else{alert('Entity%20form%20not%20detected');}})();void%200;

Below the unminified source.

(function () {
	var contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		var Xrm = contentPanels[0].contentWindow.Xrm;
		Xrm.Page.data.refresh(false).then(function () {
			Xrm.Page.data.entity.addOnSave(function (econtext) {
				var eventArgs = econtext.getEventArgs();
				if (eventArgs.getSaveMode() === 70 || eventArgs.getSaveMode() === 2) {
					eventArgs.preventDefault();
				}
			});
			alert('Form refreshed without save. Autosave turned off.');
		}, function (errorCode, message) {
			alert(message);
		});
	} else {
		alert('Entity form not detected');
	}
})();

The second bookmarklet open the selected lookup in a new window. Starting from CRM2015 every link you click inside a record form, opens in the same window. This can be annoying sometimes. To use this bookmarklet, you’ll just have to select the lookup on the record and execute the bookmarklet. This will open the lookup record in a new window. Below is the bookmarklet.

javascript:(function(){var%20contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return%20d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){var%20Xrm=contentPanels[0].contentWindow.Xrm;var%20currentControl=Xrm.Page.ui.getCurrentControl();if(currentControl.getControlType()==='lookup'){var%20currentLookup=currentControl.getAttribute().getValue();if(currentLookup){var%20entityName=currentLookup[0].type,entityId=currentLookup[0].id;var%20url=Xrm.Page.context.getClientUrl()+'/main.aspx?etc='+entityName+'&id='+entityId+'&newWindow=true&pagetype=entityrecord';window.open(url,'_blank');}}else{alert('The%20currently%20selected%20control%20is%20not%20a%20lookup');}}else{alert('Entity%20form%20not%20detected');}})();void%200;

This is the unminified source.

(function () {
	var contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		var Xrm = contentPanels[0].contentWindow.Xrm;
		var currentControl = Xrm.Page.ui.getCurrentControl();
		if (currentControl.getControlType() === 'lookup') {
			var currentLookup = currentControl.getAttribute().getValue();
			if (currentLookup) {
				var entityName = currentLookup[0].type,
				entityId = currentLookup[0].id;
				var url = Xrm.Page.context.getClientUrl() + '/main.aspx?etc=' + entityName + '&id=' + entityId + '&newWindow=true&pagetype=entityrecord';
				window.open(url, '_blank');
			}
		} else {
			alert('The currently selected control is not a lookup');
		}
	} else {
		alert('Entity form not detected');
	}
})();

References:

https://msdn.microsoft.com/en-us/library/gg509060.aspx

Convert to Case: Displaying email contents

There are two ways to create case from an incoming email:

  1. Automatic record creation rules
  2. Convert to Case button in the email command bar

converttocase.png

Once a case is created, there may be so many other activities associated with that case and the email that was used to create the case might be all the way down in the activity feed. For quick reference you might just want to see the email content right on the case itself.

If you try to copy the email’s description field that stores the email content into a multiline text field on the case, you’ll face issues because of rich text emails. Most email sent these days are HTML emails, and Dynamics CRM doesn’t have a rich text field yet. So when you copy a rich text content into a field you will get everything i.e. content+markup. For example below is a simple email sent from outlook.

outlookemail

This is what will be copied to the text field on the case, when you copy the email entity’s description field. Now I will show you an approach to do this using a HTML webresource and Javascript. Below is the code for the HTML webresource that is embedded into the Case form.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Email Body</title>
</head>
<body>
	<h3>Case Source Email</h3>
	<div id="description"></div>
</body>
<script>
	var RYR = window.RYR || {};
	var Xrm = parent.Xrm;
	
	RYR.retrieveEntityWebApi = function(entityName,recordId,additionalCriteria) {
		var headers = new Headers({
		"Accept": "application/json",
		"Content-Type": "application/json; charset=utf-8",
		"OData-MaxVersion": "4.0",
		"OData-Version": "4.0"
		});
			
		fetch("/api/data/v8.0/"+entityName+"?$select=activityid,description&$filter=_regardingobjectid_value eq "+recordId, 
		{   method: 'GET',
			headers: headers,
			credentials: 'include' //without this header, the request will fail
		})
		.then(function(response){
			return response.json();
		})
		.then(function(c){
			if(c.value && c.value.length > 0) {
				document.getElementById('description').innerHTML = c.value[0].description;
				Xrm.Page.ui.tabs.get('general').sections.get('emailsection').setVisible(true);
			}
		})
		.catch(function(err) {
			console.log(err);
		});
	};
	
	RYR.retrieveEntityOData = function(entityName,recordId,additionalCriteria,callback) {
		var odataUrl = "/XRMServices/2011/OrganizationData.svc/"+entityName+"?$select=ActivityId,Description&$filter=RegardingObjectId/Id eq (guid'"+recordId+"')";
		if(additionalCriteria) {
			odataUrl += additionalCriteria;
		}				
		var retrieveReq = new XMLHttpRequest();
		retrieveReq.open("GET", odataUrl, false);
		retrieveReq.setRequestHeader("Accept", "application/json");
		retrieveReq.setRequestHeader("Content-Type", "application/json; charset=utf-8");
		retrieveReq.onreadystatechange = function () {
			if(callback) {
				callback(this);
			}
		};
		retrieveReq.send();
	};	
	
	var processEmailResults = function (response) {
			if (response.readyState == 4) {
				var emails = JSON.parse(response.responseText).d.results;
				if(emails.length > 0) {
					document.getElementById('description').innerHTML = emails[0].Description;
					Xrm.Page.ui.tabs.get('general').sections.get('emailsection').setVisible(true);
				}
			}
	};
	
	var retrieveEmailBody = function() {
		var recordId = Xrm.Page.data.entity.getId().substr(1,36);//Get rid of '{' and '}'
		if(!window.fetch ||
			!Xrm.Page.context.getVersion ||
			Xrm.Page.context.getVersion().split('.')[0] !== '8') {
			RYR.retrieveEntityOData('EmailSet',recordId,'&$top=1&$orderby=CreatedOn',processEmailResults);
		}
		else {
			RYR.retrieveEntityWebApi('emails',recordId,'&$top=1&$orderby=CreatedOn');
		}
	};
	
	if(Xrm.Page.ui.getFormType() === 2) retrieveEmailBody();
	
	window.RYR = RYR;
</script>	
</html>

This is how it looks on the Case form

Case

As I was just experimenting with various features, I have tried two approaches: standard XHR with the old OData endpoint and using fetch API with the Web API endpoint in Dynamics CRM 2016. I believe this is a better approach going forward. You can just include fetch polyfill and start using fetch today.

I initially didn’t want to try a code based approach. I created a Quick View Form on the email entity, stored the initial email record as a lookup field in the case and then use the Quick View Form to display the email on the case. But it didn’t display the email content.

I suspect this is because of theĀ  warning “The email below might contain script or content that is potentially harmful and has been blocked.” displayed on the email. My theory is that once the email is tagged as unsafe by CRM, you cannot use Quick View Form to display its contents.

emailwarning

References:

  1. https://developer.mozilla.org/en/docs/Web/API/Fetch_API
  2. https://davidwalsh.name/fetch
  3. MSDN – Web API EntityType Reference
  4. MSDN – Retrieve an entity using Web API
  5. MSDN – Query data using Web API

Using window.postMessage to interact with IFrame in CRM Form

This is similar to my earlier post (https://dreamingincrm.com/2015/10/27/using-sessionstorage-to-interact-with-iframe-in-crm-forms/) that discussed using sessionStorage events to interact with IFrame and the IFrame to interact with the ClientApiWrapper. ClientApiWrapper is the IFrame that hosts the form scripts. Here is the code I used.

IFrame

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Form IFrame</title>
	<script>
	window.addEventListener('message', function(e) {
		if(e.origin != parent.Xrm.Page.context.getClientUrl()) return;
		document.getElementById('messageFromParent').textContent = 'Parent says "' + e.data+'"';	 
	});
		
	document.addEventListener('DOMContentLoaded' , function() {
		document.getElementById("buttons").addEventListener("click", function(e){
			var url = parent.Xrm.Page.context.getClientUrl();
			var fieldName = document.getElementById("crmFieldName").value;
			switch(e.target.id){
				case "enableField":
					parent.postMessage(['setFieldEditability', [fieldName,false]],url);
					break;
				case "disableField":
					parent.postMessage(['setFieldEditability', [fieldName,true]],url);
					break;
				case "hideField":
					parent.postMessage(['setFieldVisibility', [fieldName,false]],url);
					break;
				case "showField":
					parent.postMessage(['setFieldVisibility', [fieldName,true]],url);
					break;
			}
		});
	});
	</script>
</head>
<body>
	<p id="messageFromParent"></p><br><br>
	<input type="text" id="crmFieldName"><br><br>
	<div id="buttons">
	<button id="enableField">Enable Field</button>
	<button id="disableField">Disable Field</button>
	<button id="hideField">Hide Field</button>
	<button id="showField">Show Field</button>
	</div>	
</body>
</html>

Form Script

(function(){
var CVN = window.CVN || {};
var childChannel;
parent.addEventListener('message', function(e) {
if(e.origin != Xrm.Page.context.getClientUrl()) return;
childChannel = e.source;
CVN[e.data[0]].apply(null,e.data[1]);
}, false);

CVN.onSave = function(){
};

CVN.setFieldEditability = function(fieldName, isDisabled){
Xrm.Page.getControl(fieldName).setDisabled(isDisabled);
if(childChannel) childChannel.postMessage(fieldName+' has been '+(isDisabled ? 'disabled' : 'enabled'),Xrm.Page.context.getClientUrl());
};

CVN.setFieldVisibility = function(fieldName, isVisible){
Xrm.Page.getControl(fieldName).setVisible(isVisible)
if(childChannel) childChannel.postMessage(fieldName+' has been '+(isVisible ? 'hidden' : 'unhidden'),Xrm.Page.context.getClientUrl());
};

CVN.onLoad = function(){
};

window.CVN = CVN;
})();

Screenshot

Form

The eventhandler for message has to be attached to the parent for messages originating from both the form script (ClientApiWrapper) and the HTML webresource embedded into the form.

I envision the use of this technique primarily for form script -> html webresource communication, and common functions that are called by both form script and html webresource.

Reference: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

Using sessionStorage to interact with IFrame in CRM Forms

CRM2015 Update 1 loads form scripts in seperate IFrame called ClientApiWrapper. I have seen a couple of posts in CRM forums asking how to interact with the IFrame from the form scripts and vice versa. The usual route is “window.parent”, but I tried to do this using sessionStorage and it is doing what I want.

Form Script

(function(){
	var RYR = window.RYR || {};
	
	window.addEventListener('storage', function(e) {
		if(e.key.split(':')[0] === 'child') {
			var functionToCall = e.key.split(':')[1];
			var sessionStoredValue = JSON.parse(e.newValue);
			RYR[functionToCall]
			.apply(null,
			Object.keys(sessionStoredValue)
			.map(function(d){ 
				return sessionStoredValue[d];
			}));
			sessionStorage.removeItem(e.key);			 
		}
	});
	
	RYR.onSave = function(){
	};
	
	RYR.setFieldEditability = function(fieldName, isDisabled){
		Xrm.Page.getControl(fieldName).setDisabled(isDisabled);
	};
	
	RYR.setFieldVisibility = function(fieldName, isVisible){
		Xrm.Page.getControl(fieldName).setVisible(isVisible)
	};	
	
	RYR.onLoad = function(){
		//temp workaround: iframe storage eventhandler takes some time to attach
		setTimeout(function(){ sessionStorage.setItem('parent', 'Form Load Event..'); }, 2000);
		Xrm.Page.getAttribute('ryr_name').addOnChange(function(){ 
			sessionStorage.setItem('parent', 'Name field changed..'); 
		});
	};
	
	window.RYR = RYR;
})();

HTML Webresource embedded as form IFrame

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Form IFrame</title>
	<script>
	window.addEventListener('storage', function(e) {
		if(e.key === 'parent') {
			document.getElementById('messageFromParent').textContent = 'Parent says "' + e.newValue+'"';
			sessionStorage.removeItem(e.key);			 
		}
	});
		
	document.addEventListener('DOMContentLoaded' , function() {
		document.getElementById("buttons").addEventListener("click", function(e){
			switch(e.target.id){
				case "enableField":
					sessionStorage.setItem('child:setFieldEditability', JSON.stringify({fieldName: document.getElementById("crmFieldName").value, disabled: false}));
					break;
				case "disableField":
					sessionStorage.setItem('child:setFieldEditability', JSON.stringify({fieldName: document.getElementById("crmFieldName").value, disabled: true}));
					break;
				case "hideField":
					sessionStorage.setItem('child:setFieldVisibility', JSON.stringify({fieldName: document.getElementById("crmFieldName").value, isVisible: false}));
					break;
				case "showField":
					sessionStorage.setItem('child:setFieldVisibility', JSON.stringify({fieldName: document.getElementById("crmFieldName").value, isVisible: true}));
					break;
			}
		});
	});		
	</script>
</head>
<body>
<input type="text" id="crmFieldName">




<div id="buttons">
<button id="enableField">Enable Field</button>
<button id="disableField">Disable Field</button>
<button id="hideField">Hide Field</button>
<button id="showField">Show Field</button>
</div>




</body>
</html>

There are event listeners on both the form script as well as the IFrame. The IFrame puts values in keys in this form: “child:{function name on form script}”. The form script uses just one key: parent.

Screenshots

FormLoad

NameChanged

With this technique, the form script doesn’t have to directly manipulate anything on the embedded IFrame or vice versa. In my example, I have the IFrame storing JSON to the sessionStore. This is retrieved by the event handler to call the correct function in the form script. The parent form script simply stores a string to the session store and this is displayed in the IFrame. This has to be changed if the requirements are different.

Reference: https://developer.mozilla.org/en/docs/Web/API/Window/sessionStorage