GooglePickerAPI in ServiceNow UIPage

Kingstan M
Kilo Sage

Greetings, ServiceNow Community.

 

I am facing a challenge in GooglePickerAPI on ServiceNow UI Page. Well, the code renders but the popUP for auth., won't come. But when i save the code as html doc n run on my liveServer then it is all fine. I do not know where am wrong. Any advise?

 

UIPageCodeBelow

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">

	<html>

	<head>
		<title>Picker API Quickstart</title>
		<meta charset="utf-8" />
	</head>

	<body>
		<p>Picker API API Quickstart</p>

		<!--Add buttons to initiate auth sequence and sign out-->
		<button id="authorize_button" onclick="handleAuthClick()">Authorize</button>
		<button id="signout_button" onclick="handleSignoutClick()">Sign Out</button>

		<pre id="content" style="white-space: pre-wrap;"></pre>
		<div id="pickerDiv" class="picker-dialog"
			style="height: 500px; width:800px; border: 1px solid red; position: relative; top: 50%;"></div>

		<script type="text/javascript">
			const SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly';

        const CLIENT_ID =  // clientID
        const API_KEY = // apiKey
        const APP_ID =  // projectNumber

        let tokenClient;
        let accessToken = null;
        let pickerInited = false;
        let gisInited = false;


        document.getElementById('authorize_button').style.visibility = 'hidden';


        function gapiLoaded() {
            gapi.load('picker', intializePicker);
        }


        function intializePicker() {
            pickerInited = true;
            maybeEnableButtons();
        }


        function gisLoaded() {
            tokenClient = google.accounts.oauth2.initTokenClient({
                client_id: CLIENT_ID,
                scope: SCOPES,
                callback: '', // defined later
            });
            gisInited = true;
            maybeEnableButtons();
        }


        function maybeEnableButtons() {
            if (pickerInited &amp; gisInited) {
                document.getElementById('authorize_button').style.visibility = 'visible';
            }
        }


        function handleAuthClick() {
            tokenClient.callback = async (response) => {
                if (response.error !== undefined) {
                    throw (response);
                }
                accessToken = response.access_token;
                document.getElementById('signout_button').style.visibility = 'visible';
                document.getElementById('authorize_button').innerText = 'Refresh';
                await createPicker();
            };

            if (accessToken === null) {
                tokenClient.requestAccessToken({ prompt: 'consent' });
            } else {
                tokenClient.requestAccessToken({ prompt: '' });
            }
        }





        function createPicker() {


            const view = new google.picker.View(google.picker.ViewId.DOCS);
            view.setMimeTypes("application/pdf,image/png,image/jpeg,image/jpg,image/gif,image/svg+xml,application/vnd.ms-excel,application/vnd.google-apps.spreadsheet,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/msword,application/vnd.google-apps.document,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,video/x-flv,video/mp4,video/quicktime,video/mpeg,video/x-matroska,video/x-ms-asf,video/x-ms-wmv,video/avi,audio/mpeg,audio/wav,audio/ac3,audio/aac,audio/ogg,audio/oga,audio/x-m4a,application/zip")

            const picker = new google.picker.PickerBuilder()
                .enableFeature(google.picker.Feature.NAV_HIDDEN)
                .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
                .setDeveloperKey(API_KEY)
                .setAppId(APP_ID)
                .setOAuthToken(accessToken)
                .addView(view)
                .addView(new google.picker.DocsUploadView())
                .setCallback(pickerCallback)

            picker.build();

            const myDiv = document.getElementById('pickerDiv');
            const iframe = document.createElement('iframe');
            iframe.setAttribute("src", picker.toUri());
            iframe.style.width = "100%";
            iframe.style.height = "100%";
            myDiv.appendChild(iframe)

        }


        function pickerCallback(data) {

            if (data.action === google.picker.Action.PICKED) {

                document.getElementById('content').innerText = JSON.stringify(data, null, 2);

            }
        }
		</script>
		<script async='true' src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
		<script async='true' src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>

		<style>
			.picker-dialog {
				border: 1px solid red;
			}
		</style>
	</body>

	</html>


</j:jelly>

 

UIPageTryIt.png

 

Many thanks,

Kingstan.

 

 

1 ACCEPTED SOLUTION

-O-
Kilo Patron

To provide an example, this is how I would do it:

- create a record in table content_css, where field Style contains:

.x-content {
	white-space: pre-wrap;
}

.x-picker-dialog {
	border: 1px solid red;
	height: 500px;
	margin: auto;
	position: relative;
	top: 50%;
	width: 800px;
}

- create record in table sys_ui_script (UI Script) where field API Name contains googleLibrary and field Script contains the script originally in field HTML of the UI Page:

const SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly';
const CLIENT_ID = '1';
const API_KEY = '1';
const APP_ID = '1';

...

function pickerCallback (data) {
	if (data.action === google.picker.Action.PICKED) {
		document.getElementById('content').innerText = JSON.stringify(data, null, 2);
	}
}

- modify the UI Page's field HTML to contain:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
	<html>

	<head>
		<!-- use external style sheet (sys_id e8fba397978a3150594ffbc71153af5d is that of a record in table content_css) -->
		<link href="e8fba397978a3150594ffbc71153af5d.cssdbx?v=1.0" rel="stylesheet" />
		<title>${HTML: gs.getMessage('Picker API Quickstart') }</title>
		<meta charset="utf-8" />
	</head>

	<body>
		<!-- use external script (moniker googleLibrary is field name of the record in table sys_ui_script that contains the script -->
		<script src="googleLibrary.jsdbx?v=1.0" type="text/javascript"></script>

		<p>${HTML: gs.getMessage('Picker API API Quickstart') }</p>

		<!-- Add buttons to initiate auth sequence and sign out -->
		<button id="authorize_button" onclick="handleAuthClick()">${HTML: gs.getMessage('Authorize') }</button>
		<button id="signout_button" onclick="handleSignoutClick()">${HTML: gs.getMessage('Sign Out') }</button>

		<pre id="content" class="x-content"></pre>

		<div id="pickerDiv" class="x-picker-dialog"></div>

		<script async="true" onload="gapiLoaded()" src="https://apis.google.com/js/api.js"></script>
		<script async="true" onload="gisLoaded()" src="https://accounts.google.com/gsi/client"></script>
	</body>

	</html>
</j:jelly>

Note that I have updated the class names to not conflict with already defined class names (e.g picker-dialog to x-picker-dialog). I have also added Jelly expressions to translate texts. The keyword HTML in the Jelly expressions (${HTML: ...}) indicates to the (Jelly) processor that the expression's result should be escaped for HTML. The GUID e8fba397978a3150594ffbc71153af5d used in attribute href of the link element is the Sys ID of the style record in table content_css. File extensions .cssdbx and .jsdbx are the way to indicate to ServiceNow that it should look up a record by Sys ID in table content_css and by API Name in table sys_ui_script respectively. Also adding tags like html, head, title, meta and body only make sense if you mark the UI Page as a Direct one.

View solution in original post

12 REPLIES 12

-O-
Kilo Patron

You have a whole lot of script in that XML which may contain special characters that aren't interpreted as you might think those are. E.g. the ampersand in the if will end up literally in the script - which will make the whole script be invalid, of course. Also easy to check, open up the browser console.

 

All in all one should move out scripts into UI Script records and add those to the page using a <script> or a <g:require> tag.

E.g. if one creates a UI Script record and names it "googleLibrary", one will be able to reference it by the name "googleLibrary.jsdbx":

 

		<script src="googleLibrary.jsdbx?v=1.0" type="text/javascript"></script>

 

 

Not related but good to know: ServiceNow emits all resources (e.g. script files), so that when referenced just by name those will look to browsers as never changing and thus the 1st loaded version will be cached forever. To mitigate that one would do better to add a parameter to resource URIs, that will be updated whenever the referenced resource (script) record is modified. I mean if one would modify UI Script googleLibrary, one would update the script tag and bump parameter v to a new version - say 2.0. Only than will the browser clear the cached variant and load the new variant of the script.

 

All in all I did just that - separate out the script into a UI Script and it seems to work, of course with authentication error - I have no valid application id, key and secret.

-O-
Kilo Patron

To provide an example, this is how I would do it:

- create a record in table content_css, where field Style contains:

.x-content {
	white-space: pre-wrap;
}

.x-picker-dialog {
	border: 1px solid red;
	height: 500px;
	margin: auto;
	position: relative;
	top: 50%;
	width: 800px;
}

- create record in table sys_ui_script (UI Script) where field API Name contains googleLibrary and field Script contains the script originally in field HTML of the UI Page:

const SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly';
const CLIENT_ID = '1';
const API_KEY = '1';
const APP_ID = '1';

...

function pickerCallback (data) {
	if (data.action === google.picker.Action.PICKED) {
		document.getElementById('content').innerText = JSON.stringify(data, null, 2);
	}
}

- modify the UI Page's field HTML to contain:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
	<html>

	<head>
		<!-- use external style sheet (sys_id e8fba397978a3150594ffbc71153af5d is that of a record in table content_css) -->
		<link href="e8fba397978a3150594ffbc71153af5d.cssdbx?v=1.0" rel="stylesheet" />
		<title>${HTML: gs.getMessage('Picker API Quickstart') }</title>
		<meta charset="utf-8" />
	</head>

	<body>
		<!-- use external script (moniker googleLibrary is field name of the record in table sys_ui_script that contains the script -->
		<script src="googleLibrary.jsdbx?v=1.0" type="text/javascript"></script>

		<p>${HTML: gs.getMessage('Picker API API Quickstart') }</p>

		<!-- Add buttons to initiate auth sequence and sign out -->
		<button id="authorize_button" onclick="handleAuthClick()">${HTML: gs.getMessage('Authorize') }</button>
		<button id="signout_button" onclick="handleSignoutClick()">${HTML: gs.getMessage('Sign Out') }</button>

		<pre id="content" class="x-content"></pre>

		<div id="pickerDiv" class="x-picker-dialog"></div>

		<script async="true" onload="gapiLoaded()" src="https://apis.google.com/js/api.js"></script>
		<script async="true" onload="gisLoaded()" src="https://accounts.google.com/gsi/client"></script>
	</body>

	</html>
</j:jelly>

Note that I have updated the class names to not conflict with already defined class names (e.g picker-dialog to x-picker-dialog). I have also added Jelly expressions to translate texts. The keyword HTML in the Jelly expressions (${HTML: ...}) indicates to the (Jelly) processor that the expression's result should be escaped for HTML. The GUID e8fba397978a3150594ffbc71153af5d used in attribute href of the link element is the Sys ID of the style record in table content_css. File extensions .cssdbx and .jsdbx are the way to indicate to ServiceNow that it should look up a record by Sys ID in table content_css and by API Name in table sys_ui_script respectively. Also adding tags like html, head, title, meta and body only make sense if you mark the UI Page as a Direct one.

Great knowledge and information.

Many thanks for this.

 

Now i am struck here @authLayer.

 

You can’t sign in because this app sent an invalid request. You can try again later or contact the developer about this issue. Learn more about this error
If you are a developer of this app, see error details.
Error 400: redirect_uri_mismatch
 
KingstanM_0-1698507534029.png

 

IDK why the redirect URL is mismatch.

I see i gave it right.

Sorry but I don't know what to say to that.