I'm messing around w/ Office Dialog for Add-Ins in JS. I've got it so far where I can open a dialog, capture input to console, run a function from a button and close the dialog box, but I can't seem to get my function to interact with Excel. It's lost context I beleive, I tried using this and I get no errors, but it doesn't work --> var context = new Excel.RequestContext().
Here is my open function and my main function and the end function.
main.js
export async function helloworld(event) {
try {
await Excel.run(async (context) => {
//Start Func
console.log("BEFORE OPEN UI");
openDialog("/yo/dist/dialog.html", 30, 20);
console.log("AFTER OPEN UI");
await context.sync()
.then(function () {
console.log("AFTER SYNC UI");
var ws = context.workbook.worksheets.getActiveWorksheet();
var range = ws.getRange("A1:D5");
range.select();
})
//End Func
await context.sync();
});
} catch (error) {
console.error(error);
}
console.log("EVENT COMPLETEED HELLOW");
//event.completed();
}
open.js
function openDialog(RelURLStr, H, W) {
Office.context.ui.displayDialogAsync(window.location.origin + RelURLStr,
{ height: H, width: W }, dialogCallback);
}
run func //this gets ran, but nothing output to worksheet and no errors.
function dobuttonrun(event) {
console.log("ENDING");
var context = new Excel.RequestContext()
var ws = context.workbook.worksheets.getActiveWorksheet();
var fakedatarng = ws.getRange("A1");
fakedatarng.values = "TEST";
return context.sync();
event.completed();
}
function getGlobal() {
return typeof self !== "undefined"
? self
: typeof window !== "undefined"
? window
: typeof global !== "undefined"
? global
: undefined;
}
const g = getGlobal();
// The add-in command functions need to be available in global scope
g.dobuttonrun = dobuttonrun;
Opening a popup has UX issues on web clients. If you need to support excel web, it will ask the user a confirmation for the dialog each time and cannot by bypassed; Excel is asking not the browser!
I strongly suggest you, to use a Popup from your UX framework. If you are using Fluent UI, the microsoft suggested framework that keeps look and feel of office please refer to this page.
In case that you need a dialog solution, keep in mind that you are running a new page /yo/dist/dialog.html you'll loose all the variables and context from your parent webpage:
If you are using any SPA javascript framework like react.js or VUE.js, another clean app will be rendered.
BTW You can achieve your need implementing a simple communication as follows:
// Office.Dialog type
let dialog;
export async function helloworld(event) {
Office.context.ui.displayDialogAsync(
'/yo/dist/dialog',
{ promptBeforeOpen: true, height: 70, width: 50, displayInIframe: false },
(result) => {
if (result.status === Office.AsyncResultStatus.Failed) {
// unable to manage status
throw new Error(result.error.message)
}
else {
dialog = result.value;
dialog.addEventHandler(Office.EventType.DialogMessageReceived, messageHandler);
}
}
)
}
/**
* Assign the project to an employee.
* #param {Object} arg - Event handler args
* #param {string} arg.message - Message as string (NOT js OBJECT)
* #param {string} arg.origin - Message Origin
*/
const messageHandler = (arg) => {
// messaging can send only strings.
// If you need to pass complex JSON you need to stringify and parse objects
const message = JSON.parse(arg.message)
if (message.type === "execution-canceled") {
// you have all props of message like message.reason
dialog.close();
return
}
if (message.type === "execution-confirmed") {
// execute than close popup
// if you need to close popup than execute, just run dialog.close()
// before doStuff() and remove finally method call
doStuff()
.then(() => console.log("EVENT COMPLETEED HELLOW"))
.catch(error => console.error(error))
.finally(() => dialog.close())
}
}
const doStuff = async () => Excel.run(async (context) => {
// you don't need context.sync() before interacting excel
var ws = context.workbook.worksheets.getActiveWorksheet();
var range = ws.getRange("A1:D5");
range.select();
await context.sync();
})
On your dialog page you need to wait for Office.initialize in order to use Office.context.ui.messaging apis, than you can simply send your object.
const send = (message) => {
Office.context.ui.messageParent(JSON.stringify(message));
}
Office.initialize = () => {
// render page or activate button
}
const onExecutionConfirmed = () => send({ type: 'execution-confirmed' })
const onExcecutionCanceled = () => send({
type: 'execution-canceled',
reason: "additional infos can be placed in any props of this serialized JSON object"
})
Here is what I finished with after #CLAudio detailed answer.
commands.js
Office.onReady(() => {
// If needed, Office.js is ready to be called
});
export async function helloworld(event) {
try {
await Excel.run(async (context) => {
//Start Func
openDialog("/yo/dist/dialog.html", 30, 20);
//End Func
await context.sync();
});
} catch (error) {
console.error(error);
}
event.completed();
}
const doStuff = async () => Excel.run(async (context) => {
// you don't need context.sync() before interacting excel
var ws = context.workbook.worksheets.getActiveWorksheet();
var range = ws.getRange("A1:D5");
range.select();
await context.sync();
})
//////////////////////////////////////////
let dialog;
function openDialog(HTMLUrl, H, W) {
Office.context.ui.displayDialogAsync(window.location.origin + HTMLUrl, { promptBeforeOpen: true, height: H, width: W, displayInIframe: false },
(asyncResult) => {
if (asyncResult.status === Office.AsyncResultStatus.Failed) {
// In addition to general system errors, there are 3 specific errors for
// displayDialogAsync that you can handle individually.
switch (asyncResult.error.code) {
case 12004:
console.log("Domain is not trusted");
break;
case 12005:
console.log("HTTPS is required");
break;
case 12007:
console.log("A dialog is already opened.");
break;
default:
console.log(asyncResult.error.message);
break;
}
} else {
dialog = asyncResult.value;
/*Messages are sent by developers programatically from the dialog using office.context.ui.messageParent(...)*/
dialog.addEventHandler(Office.EventType.DialogMessageReceived, messageHandler);
/*Events are sent by the platform in response to user actions or errors. For example, the dialog is closed via the 'x' button*/
dialog.addEventHandler(Office.EventType.DialogEventReceived, eventHandler);
}
}
)
}
function eventHandler(arg) {
// In addition to general system errors, there are 2 specific errors
// and one event that you can handle individually.
switch (arg.error) {
case 12002:
console.log("Cannot load URL, no such page or bad URL syntax.");
break;
case 12003:
console.log("HTTPS is required.");
break;
case 12006:
// The dialog was closed, typically because the user the pressed X button.
console.log("Dialog closed by user");
break;
default:
console.log("Undefined error in dialog window");
break;
}
}
/**
* Assign the project to an employee.
* #param {Object} arg - Event handler args
* #param {string} arg.message - Message as string (NOT js OBJECT)
* #param {string} arg.origin - Message Origin
*/
const messageHandler = (arg) => {
// messaging can send only strings.
// If you need to pass complex JSON you need to stringify and parse objects
const message = JSON.parse(arg.message)
console.log("message:")
console.log(message)
if (message.type === "execution-canceled") {
// you have all props of message like message.reason
dialog.close();
return
}
if (message.type === "execution-confirmed") {
// execute than close popup
// if you need to close popup than execute, just run dialog.close()
// before doStuff() and remove finally method call
doStuff()
.then(() => console.log("doStuff .then"))
.catch(error => console.error(error))
.finally(() => dialog.close())
}
}
//////////////////////////////////////////
dialog.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Contoso Task Pane Add-in</title>
<script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js"></script>
<!-- For the Office UI Fabric, go to http://aka.ms/office-ui-fabric to learn more. -->
<link rel="stylesheet" href="https://appsforoffice.microsoft.com/fabric/2.1.0/fabric.min.css">
<link rel="stylesheet" href="https://appsforoffice.microsoft.com/fabric/2.1.0/fabric.components.min.css">
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.1.min.js"></script>
<script defer="defer" src="dialog.js"></script>
</head>
<body>
<p class="ms-font-xxl ms-fontColor-neutralSecondary ms-fontWeight-semilight">Pick a number</p>
<button class="ms-Button ms-Button--primary" id="run">
<span class="ms-Button-icon"><i class="ms-Icon ms-Icon--plus"></i></span>
<span class="ms-Button-label" id="runbutton-text">RUN</span>
<span class="ms-Button-description" id="runbutton-desc">Run Func</span>
</button>
<button class="ms-Button ms-Button--primary" id="close">
<span class="ms-Button-icon"><i class="ms-Icon ms-Icon--plus"></i></span>
<span class="ms-Button-label" id="closebutton-text">EXIT</span>
<span class="ms-Button-description" id="closebutton-desc">Exit Func</span>
</button>
<form>
<label for="fname">First name:</label><br>
<input type="text" id="fname" name="fname"><br>
<label for="lname">Last name:</label><br>
<input type="text" id="lname" name="lname">
</form>
</body>
</html>
dialog.js
Office.initialize = () => {
document.getElementById("run").onclick = onExecutionConfirmed;
document.getElementById("close").onclick = onExcecutionCanceled;
}
const send = (message) => {
Office.context.ui.messageParent(JSON.stringify(message));
}
const onExecutionConfirmed = () => send({ type: 'execution-confirmed' })
const onExcecutionCanceled = () => send({
type: 'execution-canceled',
reason: "additional infos can be placed in any props of this serialized JSON object"
})
Related
I am getting undefined when I type the author name in the text box and press the button to display the quote. It seems like my button and textbox are not linked together. How can I fix this?
<!DOCTYPE html>
<html lang="en">
<head>
<title>Quotes</title>
</head>
<body>
<label for="getQuotes">Find Quotes (Type Author Name)</label><br>
<input type = "text" id="getQuotes" name="getQuotes" placeholder="Search" style="margin:10px" size="50"/><br />
<button id="FetchQuotes" onclick="getQuote()" style="margin:10px">Fetch Quotes</button>
<p id="quotes"></p>
<p id="author"></p>
<script>
async function getQuote() {
//const author = Boolean(false);
let url = 'https://jaw1042-motivate.azurewebsites.net/quote';
let author = document.getElementById('getQuotes').value;
if(author) {
url = 'https://jaw1042-motivate.azurewebsites.net/quote?author= ';
console.log(url + author);
} else {
console.log(url);
}
fetch(url)
.then(async (response) => {
if (response.ok) {
console.log("Response code: " + response.status);
} else if (response.status === 400) {
console.log("Unable to find any quotes by specified author: " + response.status);
} else {
console.log("No quotes have been loaded: " + response.status);
}
const val = await response.json();
console.log(val);
}).then(data => {
document.getElementById('quotes').value = data;
document.getElementById('author').value = data;
console.log(data);
alert(data);
});
}
</script>
</body>
</html>
your then functions are not correct
in the direct result of the fetchAPI you can receive data and to use it you need to run .json() or .text() on it, you can't simply use that result or return it's value ( plus when you use return statement all your next codes will be unreachable)
after that you should not assign something to your data variable because it just has new Data fetched from backend, by assigning new value to data you're about to ruin new data
here is how your js should look
function getQuote() {
fetch("https://krv1022-motivate.azurewebsites.net/quote")
.then( res => res.text() )
.then( data => {
document.querySelector(".quote").value = data;
}
);
}
I also provided a fiddle for it but it can't receive data because either your URL is not correct or there is CORS issues
==============================================
one thing that I just noticed, you are receiving Author's name from end user but you are not about to send it to backend!
so perhaps this code is more complete, I assume that you want to send data using GET method and backend wants the name of author to be named getQuotes
I am trying to load multiple files using jQuery's get function. A minimal example looks like this:
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
<select name="file-select" id="file-select" multiple>
<option>http://url.to.file/Test1.txt</option>
<option>http://url.to.file/Test2.txt</option>
</select>
<script>
$(document).ready(function() {
$('#file-select').change(function () {
let loaded_files_list = new Array();
let promise = new Promise(function(myResolve,myReject) {
$("#file-select option:selected").each(function () {
file_name = $(this).val();
$.get($(this).val(), function(response) {
data = response;
// Do something with data
console.log("file select length: "+$("#file-select option:selected").length);
loaded_files_list.push(file_name);
console.log('loaded files length: '+loaded_files_list.length)
if(loaded_files_list.length == $("#file-select option:selected").length) {
console.log('Entered if clause');
myResolve(loaded_files_list);
}
else {
myReject('List is not complete yet');
}
});
});
});
promise.then(
function(value) {
console.log('List is complete!');
console.log(value);
},
function(error) {
console.log(error);
}
);
});
});
</script>
</body>
</html>
Now if I select a single file this works just like expected and I can see the list with a single item in the console. However if I I select both files, the if clause is entered after both files are loaded, but it seems myResolve is not called as neither List is complete! is displayed nor the list with two elements. What am I doing incorrect here?
$(document).ready(() => {
// make this a function that "waits" for the await command when
// used on promises
$('#file-select').change(async () => {
// declare an array that we will use to store our HTTP requests
const filesList = [];
// extract the clicked options
const selectedOpts = $('#file-select').children('option:selected')
// loop over the clicked options
selectedOpts.each((index, opt) => {
// extract the text value from the option
const link = opt.text
// push the HTTP request to the filesList array without
// waiting for the response
filesList.push($.get(link))
})
// wait for the HTTP requests to to complete before
// continuing
const responses = await Promise.all(filesList)
// loaded all files
responses.forEach(data => {
console.log(data) // => file contents
})
});
});
I am working on a WebApp Add-on in Google Sheets. Below is the Addon and webapp script. I want to read validation/error message from webapp and display to user.
Like I will send message from doPost(e) like "Check Values" and user should get this as message box.
function copyData() {
var ss_id = SpreadsheetApp.getActive().getId();
//This is the Web App URL.
var url = "https://script.google.com/macros/s/<id>/exec";
var payload = {
"ss_id" : ss_id, // Modified
}
var options = {
"method" : "POST",
"payload" : payload,
"followRedirects" : true,
"muteHttpExceptions" : true,
};
var result = UrlFetchApp.fetch(url, options);
}
function doPost(e) {
var ss_id = e.parameter.ss_id; // Modified
var response = {
"status" : "FAILED",
"ss_id" : ss_id,
};
//var ss_id = ss_id[0];
//Use your spreadsheetID to get Output Sheet
var Manager_SS=SpreadsheetApp.openById('<id>');
var Manager_Sheet=Manager_SS.getSheetByName('Consolidated_Data');
var FrontDesk_ss = SpreadsheetApp.openById(ss_id);
var FrontDesk_sheet = FrontDesk_ss.getSheetByName('Data');
//Get front desk data
var sData = FrontDesk_sheet.getRange("A2:C10").getValues();
//Copy data from Front Desk to Manager Sheet.
Manager_Sheet.getRange("A2:C10").clear();
Manager_Sheet.getRange("A2:C10").setValues(sData);
//Update done after copying data.
FrontDesk_sheet.getRange('D1:D10').setValue('Done');
var response = {
"status" : "SUCCESS",
"sData" : sData,
};
return ContentService.createTextOutput(JSON.stringify(response));
}
For this example I am using a bounded script, but this should be the same for an Editor Add-on
In the spreadsheet we want to validate, we create a custom menu to call a function that makes a POST request to our Web App. Depending on the response, we display one content or another.
const UI = SpreadsheetApp.getUi()
const onOpen = () => {
/* Adds the custom menu */
UI.createMenu('Custom Function').addItem('Is Valid?', 'checkValidity').addToUi()
}
const checkValidity = () => {
const res = UrlFetchApp.fetch
(
/* Change it for your URL */
"https://script.google.com/macros/s/<ID>/exec",
{
"method": "post",
"contentType": "application/json",
/* In this example I only send the ID of the Spreadsheet */
"payload": JSON.stringify(
{
"ss_id": SpreadsheetApp.getActiveSpreadsheet().getId()
}
)
}
)
/* Depending on the response from the Web App */
/* We show different messages */
const { MSG } = JSON.parse(res.getContentText())
UI.alert(MSG === "OK" ? "IS VALID" : "IS NOT VALID")
}
After we create a Web App that validates the ID. In this example I am only validating that the ID is contained in an array of valid IDs, but this should be replaced by whatever you need. As a response I only send a simple "OK" or "NOT OK", but this can be replaced with any kind of data.
const doPost = (e) => {
/* We perform the checks we need */
/* In this example only checking if the id is contained in an array */
/* This should be changed to perform the desired checks */
const validID = ["<VALID_ID_1>","<VALID_ID_2>"]
const { ss_id } = JSON.parse(e.postData.contents)
/* SpreadsheetApp.openById(ss_id).copy("my_new_copy") */
const checker = validID.includes(ss_id)
/* We send back the response */
/* Depending on the checker value */
return ContentService.createTextOutput(JSON.stringify(
{
"MSG": checker ? "OK" : "NOT OK"
}
)).setMimeType(ContentService.MimeType.JSON)
}
Reading Validation Error from Webapp
html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<form>
<input type="text" id="txt1" name="id" placeholder="Enter Numbers only"/>
<input type="button" value="submit" onClick="processForm(this.parentNode);" />
</form>
<script>
function processForm(obj) {
console.log(obj.id.value);
if(obj.id.value.match(/[A-Za-z]/)) {
google.script.run.displayError("Invalid Characters Found in id field");
} else {
google.script.run.sendData(obj);
}
}
</script>
</body>
</html>
GS:
function doPost(e) {
Logger.log(e.postData.contents);
Logger.log(e.postData.type);
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet1");
let data = JSON.parse(e.postData.contents);
let row = [];
Object.keys(data).forEach(k => row.push(data[k]));
Logger.log(JSON.stringify(row))
sh.appendRow(row);
}
function sendData(obj) {
const url = ScriptApp.getService().getUrl();
const params={"contentType":"application/json","payload":JSON.stringify(obj),"muteHttpExceptions":true,"method":"post","headers": {"Authorization": "Bearer " + ScriptApp.getOAuthToken()}};
UrlFetchApp.fetch(url,params);
}
function displayError(msg) {
SpreadsheetApp.getUi().alert(msg);
}
function launchMyDialog() {
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutputFromFile('ah1'),'My Dialog');
}
With a QR code vcard, the user scans the code with their phone and then the dialog with the "add to contacts" pops up on their phone, such as the code below:
How can I do the same but instead of a QR code scan, I want it to do the same with a button click.
I have tried the following:
var btn = document.getElementById(“clickMe”);
btn.addEventListener(“click”, loadvcard);
function loadvcard(){
url = "BEGIN%3AVCARD%0AVERSION%3A3.0%0AN%3ADoe%3BJohn%0AFN%3AJohn%20Doe%0ATITLE%3A08002221111%0AORG%3AStackflowover%0AEMAIL%3BTYPE%3DINTERNET%3Ajohndoe%40gmail.com%0AEND%3AVCARD";
window.open(url);
}
You can open your vcard in the browser as a data url if you want.
Your code would be:
var btn = document.getElementById(“clickMe”);
btn.addEventListener(“click”, loadvcard);
function loadvcard(){
var data = "BEGIN%3AVCARD%0AVERSION%3A3.0%0AN%3ADoe%3BJohn%0AFN%3AJohn%20Doe%0ATITLE%3A08002221111%0AORG%3AStackflowover%0AEMAIL%3BTYPE%3DINTERNET%3Ajohndoe%40gmail.com%0AEND%3AVCARD";
window.open("data:text/x-vcard;urlencoded," + data);
}
Try to use the web share api, it works.
<html>
<title>
Web Share API
</title>
<body>
<div>
<div>
<button onclick="shareVcard" id="shareFilesButton">Share Files</button>
</div>
</div>
</body>
<script>
document.getElementById('shareFilesButton').addEventListener("click", () => shareVcard())
function shareVcard() {
fetch("sample.vcf")
.then(function(response) {
return response.text()
})
.then(function(text) {
var file = new File([text], "sample.vcf", {type: 'text/vcard'});
var filesArray = [file];
var shareData = { files: filesArray };
if (navigator.canShare && navigator.canShare(shareData)) {
// Adding title afterwards as navigator.canShare just
// takes files as input
shareData.title = "vcard";
navigator.share(shareData)
.then(() => console.log('Share was successful.'))
.catch((error) => console.log('Sharing failed', error));
} else {
console.log("Your system doesn't support sharing files.");
}
});
}
</script>
</html>
I'm using the samples for the MSAL and converting them to use MS Graph to read SharePoint but when it comes to reading list items it seems I am getting permissions issues.
To make sure I have my syntax correct, I use the Graph Explorer with my AD account and I am able to read list items and confirm the URI is correct. I am also able to read and get an array of lists. But as soon as I try to get the list items for a list nothing is returned.
The base code is here https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-javascript-spa
Here's the code I converted from the sample. If you update the variables and register in Azure you should be able to run against your SPO site.
<!DOCTYPE html>
<html>
<head>
<title>Quickstart for MSAL JS</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.3.4/bluebird.min.js"></script>
<script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/msal.js"></script>
</head>
<body>
<h2>Welcome to MSAL.js Quickstart</h2><br />
<h4 id="WelcomeMessage"></h4>
<button id="SignIn" onclick="signIn()">Sign In</button><br /><br />
<button id="btnAllLists" onclick="GetWithEndPoint()">Get All Lists</button><br /><br />
<button id="btnListItems" onclick="GetWithEndPoint()">Get List Items</button><br /><br />
<button id="btnListItemsAllFields" onclick="GetWithEndPoint()">Get List Items All Fields</button><br /><br />
<pre id="json"></pre>
<script>
var config = {
portalname: "yourportalname",
sitename: "yoursitename",
listid: "guidofalist"
}
var msalConfig = {
auth: {
clientId: "azureclientguid",
authority: "https://login.microsoftonline.com/yourportal.onmicrosoft.com"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
var graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
spShowAllListsEp: "https://graph.microsoft.com/v1.0/sites/" + config.portalname + ".sharepoint.com:/sites/" + config.sitename + ":/lists",
spShowListItemsEp: "https://graph.microsoft.com/v1.0/sites/" + config.portalname + ".sharepoint.com:/sites/" + config.sitename + ":/lists/" + config.listid + "/items",
spShowListItemsAllFieldsEp: "https://graph.microsoft.com/v1.0/sites/" + config.portalname + ".sharepoint.com:/sites/" + config.sitename + ":/lists/" + config.listid + "/items?expand=fields",
};
// this can be used for login or token request, however in more complex situations this can have diverging options
var requestObj = {
scopes: ["user.read"]
};
var myMSALObj = new Msal.UserAgentApplication(msalConfig);
// Register Callbacks for redirect flow
myMSALObj.handleRedirectCallback(authRedirectCallBack);
function callMSGraph(theUrl, accessToken, callback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200)
callback(JSON.parse(this.responseText));
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.setRequestHeader('Authorization', 'Bearer ' + accessToken);
xmlHttp.send();
}
function signIn() {
myMSALObj.loginPopup(requestObj).then(function (loginResponse) {
//Login Success
showWelcomeMessage();
acquireTokenPopupAndCallMSGraph();
}).catch(function (error) {
console.log(error);
});
}
function acquireTokenPopupAndCallMSGraph() {
//Always start with acquireTokenSilent to obtain a token in the signed in user from cache
myMSALObj.acquireTokenSilent(requestObj).then(function (tokenResponse) {
callMSGraph(graphConfig.graphMeEndpoint, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
// Upon acquireTokenSilent failure (due to consent or interaction or login required ONLY)
// Call acquireTokenPopup(popup window)
if (requiresInteraction(error.errorCode)) {
myMSALObj.acquireTokenPopup(requestObj).then(function (tokenResponse) {
callMSGraph(graphConfig.graphMeEndpoint, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
});
}
});
}
function graphAPICallback(data) {
document.getElementById("json").innerHTML = JSON.stringify(data, null, 2);
}
function showWelcomeMessage() {
var divWelcome = document.getElementById('WelcomeMessage');
divWelcome.innerHTML = 'Welcome ' + myMSALObj.getAccount().userName + "to Microsoft Graph API";
var loginbutton = document.getElementById('SignIn');
loginbutton.innerHTML = 'Sign Out';
loginbutton.setAttribute('onclick', 'signOut();');
var btn1 = document.getElementById('btnAllLists');
btn1.setAttribute('onclick', "GetWithEndPoint('" + graphConfig.spShowAllListsEp + "');");
var btn2 = document.getElementById('btnListItems');
btn2.setAttribute('onclick', "GetWithEndPoint('" + graphConfig.spShowListItemsEp + "');");
var btn3 = document.getElementById('btnListItemsAllFields');
btn3.setAttribute('onclick', "GetWithEndPoint('" + graphConfig.spShowListItemsAllFieldsEp + "');");
}
//This function can be removed if you do not need to support IE
function acquireTokenRedirectAndCallMSGraph() {
//Always start with acquireTokenSilent to obtain a token in the signed in user from cache
myMSALObj.acquireTokenSilent(requestObj).then(function (tokenResponse) {
callMSGraph(graphConfig.graphMeEndpoint, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
// Upon acquireTokenSilent failure (due to consent or interaction or login required ONLY)
// Call acquireTokenRedirect
if (requiresInteraction(error.errorCode)) {
myMSALObj.acquireTokenRedirect(requestObj);
}
});
}
function authRedirectCallBack(error, response) {
if (error) {
console.log(error);
}
else {
if (response.tokenType === "access_token") {
callMSGraph(graphConfig.graphEndpoint, response.accessToken, graphAPICallback);
} else {
console.log("token type is:" + response.tokenType);
}
}
}
function requiresInteraction(errorCode) {
if (!errorCode || !errorCode.length) {
return false;
}
return errorCode === "consent_required" ||
errorCode === "interaction_required" ||
errorCode === "login_required";
}
function signOut() {
myMSALObj.logout();
}
// Browser check variables
var ua = window.navigator.userAgent;
var msie = ua.indexOf('MSIE ');
var msie11 = ua.indexOf('Trident/');
var msedge = ua.indexOf('Edge/');
var isIE = msie > 0 || msie11 > 0;
var isEdge = msedge > 0;
//If you support IE, our recommendation is that you sign-in using Redirect APIs
//If you as a developer are testing using Edge InPrivate mode, please add "isEdge" to the if check
// can change this to default an experience outside browser use
var loginType = isIE ? "REDIRECT" : "POPUP";
if (loginType === 'POPUP') {
if (myMSALObj.getAccount()) {// avoid duplicate code execution on page load in case of iframe and popup window.
showWelcomeMessage();
acquireTokenPopupAndCallMSGraph();
}
}
else if (loginType === 'REDIRECT') {
document.getElementById("SignIn").onclick = function () {
myMSALObj.loginRedirect(requestObj);
};
if (myMSALObj.getAccount() && !myMSALObj.isCallback(window.location.hash)) {// avoid duplicate code execution on page load in case of iframe and popup window.
showWelcomeMessage();
acquireTokenRedirectAndCallMSGraph();
}
} else {
console.error('Please set a valid login type');
}
</script>
<script>
function GetWithEndPoint(endpointString) {
myMSALObj.acquireTokenSilent(requestObj).then(function (tokenResponse) {
callMSGraph(endpointString, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
if (requiresInteraction(error.errorCode)) {
myMSALObj.acquireTokenPopup(requestObj).then(function (tokenResponse) {
callMSGraph(endpointString, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
});
}
});
}
</script>
</body>
</html>
Clicking either button that returns list items throws this message which I understand to mean is permissions.
{
"#odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.list)('myid')/items",
"value": []
}
My expectation is that I would get the same results from the Graph Explorer. But this indicates I don't have permission. I've tried a few different lists and the results are always the same. I can get a list of all the lists. But trying to get the items from a list fails.
Can we not use the Graph API with JS to get list items?
Here are the Azure delegated permissions from Azure which I think should be all I need to get list items.
But this indicates I don't have permission
That's right, empty results usually indicates one of the following permissions (delegated permissions in your case) are missing for Get Items endpoint:
Sites.Read.All - read items in all site collections
Sites.ReadWrite.All - edit or delete items in all site collections