SharePoint Online- Bad service principal breaks SharePoint Admin API Access “Global Service Principal ID Error: Error: {}” Resolved

Could not retrieve Global Service Principal ID Error

We recently ran into an issue when creating SPFX Azure Active Directory App registrations using an O-Auth Implicit flow that our SharePoint Admin Center API Access page then gave an error:

Could not retrieve Global Service Principal ID Error: Error: {}
Could not retrieve Global Service Principal ID Error: Error: {}
Could not retrieve Global Service Principal ID Error: Error: {}

And when using O365 CLI, I was getting the same error trying to add a new serviceprincipal or list them:

o365$ spo serviceprincipal grant list
Error: Another object with the same value for property identifierUris already exists.

Error: Another object with the same value for property identifierUris already exists.
Error: Another object with the same value for property identifierUris already exists.

So now I am completely locked out of interacting with SharePoint Online to Azure AD Applications until this is fixed.

I opened a case with SharePoint Online MS Support and they said its on the Azure side but they would look into it for me.

After a few days they got back to me and said it was due to a bad Azure AD App Registration tagged under SharePoint that is causing the error. GREAT! I now have a place to start.

  1. Go into Azure Active Directory and go to App Registrations and search for “SharePoint”. Your “broken app” is somewhere in one of these two registrations.

    App registrations for SharePoint
    App registrations for SharePoint
  2. Big thanks to this article where it says how to find the bad one, https://github.com/SharePoint/sp-dev-docs/issues/3891#issuecomment-494868401 
    In the API Access admin page of SP admin https://yourcompany-admin.sharepoint.com/_layouts/15/online/AdminHome.aspx#/webApiPermissionManagement, go to view source and search for spfx3rdPartyServicePrincipalId. You will find a GUID, THIS IS THE GOOD ONE. The other application is bad.
    Good one (found in view source of Admin page- “spfx3rdPartyServicePrincipalId”:”ebxxxx-xxxxx-xxxxxxx899d1
  3. No go back into app registrations and map out what GUIDs belong where
    Good ID: “SharePoint Online Client Extensibility Web Application Principal
  4. That means the other one is bad! “SharePoint Online Client Extensibility Web Application Principal Helper
    According to the above article, deleting this one fixes it. AND IT DID!
    Object ID 218 is the good one, and the top one 421 is BAD!
  5. Sure enough, after removing the bad application I can now access the API Access page!

So after removing every single bad VM, app registration or enterprise app, it all comes down to removing the bad “SharePoint Online Client Extensibility Web Application Principal Helper” app registration above. Now everything works as expected and I can approve my app requests!

Big lesson learned!

Now I can do a Enable-SPOTenantServicePrincipal without errors:

Hope this helps and leave any comments below on your experience with this error.

Create a SPFx web part to display SharePoint list data based on locale and Graph API data

This is an example of how I used the SharePoint Framework (SPFx) to create a modern page/modern experience web part that shows SharePoint list data based on a user’s Language/Locale/Country and some other user profile information from Azure Active Directory using the Graph API.

We onboard guest users using Azure AD B2B to our SharePoint Online tenant using a PowerShell script I documented before. We add some additional profile properties in the CSV when we onboard them, such as Office Location (Which we put whatever we want in here), etc. The issue is, SharePoint Online SPFx web parts do not have a way to pull this information. So, we use the Graph API.

User Data

SPFx Web Part

SharePoint List data

Comparison

Display comparison results in web part

This code will even create the SharePoint list for you using an elements.xml/schema.xml file.

There are some holes in the data structures and list columns since I removed some that are client specific. I did my best to match them up.

Solution

    1. Setup your SPFx environment https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-development-environment
  1. Install dependencies for jQuery and Bootstrap (if needed). You only need to run npm install if you don’t use the Yeoman generator (this creates the node_modules folder in your solution. Since these files are not in TFS, I run npm install on new environments when grabbing from source control):

[code language=”bash”]
npm install

npm install –save jquery@2

npm install –save bootstrap
[/code]

  1. Open SPFXProject1\config\config.json and add the path for jQuery and Bootstrap.

[code language=”text”]

"externals": {
"jquery": {
"path": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js",
"globalName": "jquery"
},
"bootstrap": {
"path": "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js",
"globalName": "bootstrap"
}
},

[/code]

  1. Open SPFXProject1\config\package-solution.json and add the Elements/schema assets reference so the SharePoint list can be created. This creates a feature that deploys the list, which is good to know for uninstalling/troubleshooting.

[code language=”text”]…
"features": [
{
"title": "My Web Part Create List Feature",
"description": "Deploys a list named My List for the slider web part, a content type and site columns",
"id": "88d9ea22-0988-4c58-b551-b1c5dc92e211",
"version": "1.0.0.0",
"assets": {
"elementManifests": [ "elements.xml" ],
"elementFiles": [ "schema.xml" ]
}
}
]
…[/code]

  1. Open SPFXProject1\src\webparts\mywebpartname\MySliderWebPart.manifest.json file and add the full width web part zone hack from https://blog.velingeorgiev.com/how-add-spfx-webpart-full-width-column

[code language=”text”]…

"manifestVersion": 2,
"supportsFullBleed": true,

[/code]

  1. Create the elements.xml file in SPFXProject1\SharePoint\assets\elements.xml

[code language=”xml”]<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Title="MyWebPart"
Location="ClientSideExtension.ApplicationCustomizer"
ClientSideComponentId="8ec40281-8c15-4a97-ab64-cc1feeb7a50c"
ClientSideComponentProperties="{"testMessage":"Test message"}">
</CustomAction>
<Field ID="{3246929A-5ECA-4E1E-BC8F-4ED878F3A33A}"
Name="SPFxSliderImage"
DisplayName="Slider Image"
Type="Image"
RichText="TRUE"
RichTextMode="FullHtml"
Group="Homepage Slider"/>
<Field ID="{1837EB2A-0A2B-494B-9509-61AEDF7BB2CB}"
Name="SPFxSliderBGImage"
DisplayName="Slider BG Image"
Type="Image"
RichText="TRUE"
RichTextMode="FullHtml"
Group="Homepage Slider"/>
<Field ID="{CEEFD3D4-212F-48D7-AA8F-B9A23A7C0FFC}"
Name="SPFxOrder"
DisplayName="Order"
Type="Number"
Group="Homepage Slider" />
<Field ID="{1F2EEECC-5AB3-4D46-8656-E54FDEA3A08D}"
Name="SPFxDesc"
DisplayName="Description"
Type="Text"
Group="Homepage Slider" />
<Field ID="{683FD699-2404-4E6B-848C-100E8CF8D38E}"
Name="SPFxBtnLeftTxt"
DisplayName="Button Left Text"
Type="Text"
Group="Homepage Slider" />
<Field ID="{E3312F89-8492-4A54-9129-16170D64DD6F}"
Name="SPFxBtnLeftURL"
DisplayName="Button Left URL"
Type="Text"
Group="Homepage Slider" />
<Field ID="{9801966F-7B8F-4214-83CA-35C83D41A04A}"
Name="SPFxBtnRightTxt"
DisplayName="Button Right Text"
Type="Text"
Group="Homepage Slider" />
<Field ID="{1ECB4C39-8E31-491C-8A5C-6701C458518B}"
Name="SPFxBtnRightURL"
DisplayName="Button Right URL"
Type="Text"
Group="Homepage Slider" />
<Field ID="{8A327CB8-578A-433F-9F00-75D92B13825C}"
Name="SPFxSliderExpire"
DisplayName="Expires"
Type="DateTime"
Format="DateOnly"
Group="Homepage Slider" />
<ContentType ID="0x01009FAF6E59182F44F6AABD475FDC8F883A"
Name="Slider"
Group="My Company Content Types"
Description="Sample content type from web part solution">
<FieldRefs>
<FieldRef ID="{3246929A-5ECA-4E1E-BC8F-4ED878F3A33A}" />
<FieldRef ID="{1837EB2A-0A2B-494B-9509-61AEDF7BB2CB}" />
<FieldRef ID="{CEEFD3D4-212F-48D7-AA8F-B9A23A7C0FFC}" />
<FieldRef ID="{1F2EEECC-5AB3-4D46-8656-E54FDEA3A08D}" />
<FieldRef ID="{683FD699-2404-4E6B-848C-100E8CF8D38E}" />
<FieldRef ID="{E3312F89-8492-4A54-9129-16170D64DD6F}" />
<FieldRef ID="{9801966F-7B8F-4214-83CA-35C83D41A04A}" />
<FieldRef ID="{1ECB4C39-8E31-491C-8A5C-6701C458518B}" />
<FieldRef ID="{8A327CB8-578A-433F-9F00-75D92B13825C}" />
</FieldRefs>
</ContentType>
<ListInstance
CustomSchema="schema.xml"
FeatureId="00bfea71-de22-43b2-a848-c05709900100"
Title="My List"
Description="My List for site"
TemplateType="100"
Url="Lists/MyList">
</ListInstance>
</Elements>[/code]

The above template Types are 100 for Custom List.

 

  1. Create schema.xml with your list columns from the above elements.xml SPFXProject1\SharePoint\assets\schema.xml

[code language=”xml”]<List xmlns:ows="Microsoft SharePoint" Title="Basic List" EnableContentTypes="TRUE" FolderCreation="FALSE" Direction="$Resources:Direction;" Url="Lists/Basic List" BaseType="0" xmlns="http://schemas.microsoft.com/sharepoint/">

<MetaData>
<ContentTypes>
<ContentTypeRef ID="0x01009FAF6E59182F44F6AABD475FDC8F883A" />
</ContentTypes>
<Fields></Fields>
<Views>
<View BaseViewID="1" Type="HTML" WebPartZoneID="Main" DisplayName="$Resources:core,objectiv_schema_mwsidcamlidC24;" DefaultView="TRUE" MobileView="TRUE" MobileDefaultView="TRUE" SetupPath="pages\viewpage.aspx" ImageUrl="/_layouts/images/generic.png" Url="AllItems.aspx">
<XslLink Default="TRUE">main.xsl</XslLink>
<RowLimit Paged="TRUE">30</RowLimit>
<Toolbar Type="Standard" />
<ViewFields>
<FieldRef Name="LinkTitle"></FieldRef>
<FieldRef Name="SPFxSliderImage"></FieldRef>
<FieldRef Name="SPFxSliderBGImage"></FieldRef>
<FieldRef Name="SPFxOrder"></FieldRef>
<FieldRef Name="SPFxDesc"></FieldRef>
<FieldRef Name="SPFxBtnLeftTxt"></FieldRef>
<FieldRef Name="SPFxBtnLeftURL"></FieldRef>
<FieldRef Name="SPFxBtnRightTxt"></FieldRef>
<FieldRef Name="SPFxBtnRightURL"></FieldRef>
<FieldRef Name="SPFxSliderExpire"></FieldRef>
</ViewFields>
<Query>
<OrderBy>
<FieldRef Name="ID" />
</OrderBy>
</Query>
</View>
</Views>
<Forms>
<Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
<Form Type="EditForm" Url="EditForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
<Form Type="NewForm" Url="NewForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
</Forms>
</MetaData></List>

[/code]

  1. Add import statements to your SPFXProject1\src\webparts\mywebpartname\MySliderWebPart.ts code in Visual Studio so we can reference the above jQuery and Bootstrap code:

[code language=”javascript”]import * as jQuery from ‘jquery’;
import * as bootstrap from ‘bootstrap’;[/code]

  1. Import IWebPartContext from the @microsoft/sp-webpart-base so we can get the users locale from their browser session:

[code language=”javascript”]import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
IWebPartContext
} from ‘@microsoft/sp-webpart-base’;
import { SPComponentLoader } from ‘@microsoft/sp-loader’;[/code]

  1. Import intersection from lodash so we can compare where two arrays intersect easily and the Graph API. Note, the day after I wrote this web part, MS released a new Graph API for SPFx. See link in comment to do it the newer way (recommended):

[code language=”javascript”]// GraphHttpClient is in preview and will be depricated soon. Use new method https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-msgraph
import { SPHttpClient, SPHttpClientResponse, GraphHttpClient, GraphHttpClientResponse } from ‘@microsoft/sp-http’;
import { escape, intersection } from ‘@microsoft/sp-lodash-subset’;[/code]

  1. Now we can jump into setting up the data structures:

[code language=”javascript”]

// All of the list data we get back from SharePoint, REST API response format

export interface ISlides {
value: ISlide[];
}

// Each item in the list from REST API response
export interface ISlide {
ID: number;
Title: string;
SPFxDesc: string;
SPFxBtnLeftTxt: string;
SPFxBtnLeftURL: string;
SPFxBtnRightTxt: string;
SPFxBtnRightURL: string;
SPFxSliderExpire: Date;
SPFxOrder: number;
Office: string;
Language: string;
Region: string[];
}

// Each item in the list’s Publishing HTML Image, since its an extended property we need a second rest call
export interface ISlideItem {
SPFxSliderImage: string;
SPFxSliderBGImage: string;
}

export interface IItemGuid {
value: string;
}

// each actual list item data
export interface slides {
ID: number;
Title: string;
UrlSlideImg: string;
UrlSlideBGImg: string;
SPFxDesc: string;
SPFxBtnLeftTxt: string;
SPFxBtnLeftURL: string;
SPFxBtnRightTxt: string;
SPFxBtnRightURL: string;
SPFxSliderExpire: Date;
SPFxOrder: number;
Office: string;
Language: string;
Region: string[];
}

export interface IUserProfile {
Office: Array<string>;
OfficeString: string;
Locale: string;
Language: string;
Region: string;
CountryCode: string;
}

// User profile info from Graph API
export interface IUserProfileResponse {
[property: string]: string;
}[/code]

  1. On the export statement, load your jQuery from CDN. These lines give some warnings during the gulp bundle/package process. Let me know if you know how I can fix them, but this way works.

[code language=”javascript”]

private _slides: slides[] = [];
public constructor(context: IWebPartContext) {
super();
// FIX LATER- redundant to load jquery twice, but this will load bootstrap JS and CSS. Fix this and top jQuery import
SPComponentLoader.loadCss(‘https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css’);
SPComponentLoader.loadCss(‘https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css’);

SPComponentLoader.loadScript(‘https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js’, { globalExportsName: ‘jQuery’ }).then((jQuery: any): void => {
SPComponentLoader.loadScript(‘https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js’, { globalExportsName: ‘jQuery’ }).then((): void => {});
});

}

[/code]

  1. Now create your REST API call methods (Replace Some List with your SharePoint list name)

[code language=”javascript”]
private _getCurrentUserProfile(): Promise<IUserProfileResponse> {
// Query user properties from office graph API
// Tutorial: https://docs.microsoft.com/en-us/sharepoint/dev/spfx/call-microsoft-graph-using-graphhttpclient
// Graph API properties https://github.com/microsoftgraph/microsoft-graph-docs/blob/master/api-reference/v1.0/api/user_list.md#user-content-example-2-users-request-using-select
let promise: Promise<IUserProfileResponse> = new Promise<IUserProfileResponse>((resolve, reject) => {
// Call the Rest API for current user, get AAD properties.
this.context.graphHttpClient.get("v1.0/me?$select=displayName, officeLocation, preferredLanguage, givenName, country", GraphHttpClient.configurations.v1)
.then((response: GraphHttpClientResponse) => {
//console.log("response.json(): " + response.json());
resolve(response.json());
}).catch((error: any) => {
reject(error);
});

return promise;
});
return promise;

}

private _getSlides(): Promise<ISlides> {
return this.context.spHttpClient.get(`${this.context.pageContext.web.absoluteUrl}/_api/web/lists/getByTitle(‘My List’)/items?select=Title,SPFxDesc,SPFxBtnLeftURL,SPFxBtnLeftTxt,SPFxBtnRightURL,SPFxBtnRightTxt,SPFxSliderExpire,SPFxOrder,Office,Language,Region&$orderBy=SPFxOrder asc`,
SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
return response.json();
});
}

private _getImage(id: number): Promise<ISlideItem> {
return this.context.spHttpClient.get(`${this.context.pageContext.web.absoluteUrl}/_api/web/lists/getByTitle(‘My List’)/items(‘${id}’)/FieldValuesAsHtml`,
SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
return response.json();
});
}

//private _getImageUrl(url: string): Promise<string> {
// return this.context.spHttpClient.get(`${this.context.pageContext.web.absoluteUrl}/_api/web/GetFileByServerRelativeUrl(‘${this.context.pageContext.web.serverRelativeUrl}${url}’)/ListItemAllFields/ServerRelatveUrl`,
// SPHttpClient.configurations.v1)
// .then((response: SPHttpClientResponse) => {
// return response.json();
// })
// .then((item: IItemGuid) => {
// return item.value;
// });
//}

private _itemCarouselIndicators(index: number): string {
if (index == 0) {
return `<li data-target="#myCarousel" data-slide-to="${index}" class="active" style="background-color:#d7d7d7"></li>`;
}
else {
return `<li data-target="#myCarousel" data-slide-to="${index}" class=""></li>`;
}
}

private _userProfile(aadquery: IUserProfileResponse) {
let userProfile: IUserProfile = { Office: [""], CountryCode: "", Language: "", Locale: "", OfficeString: "", Region: "" };
//console.log(userProfile);
// Store result for each property
// console.log("Graph API current user: " + result.displayName + result.officeLocation + result.preferredLanguage + result.givenName + result.country);

// 1. Office (Office)
userProfile.OfficeString = aadquery.officeLocation;
if (!userProfile.OfficeString) {
userProfile.OfficeString = "Los Angeles";
}
//console.log("Office: " + me._userprofile.OfficeString);
userProfile.Office = userProfile.OfficeString.split(",");
console.log("Users Office:" + userProfile.Office);

// 2. Locale (preferredLanguage) – OLD WAY from AAD, not good.
//userProfile.Locale = aadquery.preferredLanguage;
//if (!userProfile.Locale) {
// userProfile.Locale = "en-US";
//}
//console.log("Users Locale: " + me._userprofile.Locale);

// 3. Country Code (country)
userProfile.CountryCode = aadquery.country;
if (!userProfile.CountryCode) {
userProfile.CountryCode = "US";
}
//console.log("Users Country: " + me._userprofile.CountryCode);

// Final formatting of data

// Select current users Lanaguage (spelled out)
// from en-US, just keep the language. We use Country from AAD to determine Country/Region, not locale
let userLang: string = this.context.pageContext.cultureInfo.currentUICultureName;

userLang = userLang.substring(0, 2);

switch (userLang) {
case "en":
userProfile.Language = "English";
break;
case "es":
userProfile.Language = "Spanish";
break;
default:
userProfile.Language = "English";
}
console.log("Users Language: " + userProfile.Language);

// Select current users Region
// Define all US and CA regions, default any other country to Latin America
// Phase 2 or so, make this read from a SP lookup list.
switch (userProfile.CountryCode) {
// USA
case "US":
userProfile.Region = "USA";
break;

// United Kingdom
case "GB":
userProfile.Region = "United Kingdom";
break;

// Mexico (not really needed since default is Latin America)
case "MX":
userProfile.Region = "Latin America";
break;

// Latin America – Default anyone not US or GB to Latin America for now, since thats the 3rd region and is currently the only region with multiple countries (cases)
default:
userProfile.Region = "Latin America";
}
console.log("Users Region: " + userProfile.Region);

return userProfile;
}
private _itemSlideWrapper(item: slides, index: number): string {
// if the buttons are empty, dont display them
let leftButtonHTML: string = "";
let rightButtonHTML: string = "";
let SPFxBtnLeftTxt = item.SPFxBtnLeftTxt ? item.SPFxBtnLeftTxt : ”;
let SPFxBtnRightTxt = item.SPFxBtnRightTxt ? item.SPFxBtnRightTxt : ”;
if (SPFxBtnLeftTxt.length > 0) {
leftButtonHTML = `<a href="${item.SPFxBtnLeftURL ? item.SPFxBtnLeftURL : ‘#’}" style= "margin-right: 20px;" target= "_blank" > <div class="slider-button ${styles.sliderButton}" > ${SPFxBtnLeftTxt} </div></a>`;
}
if (SPFxBtnRightTxt.length > 0) {
rightButtonHTML = `<a href="${item.SPFxBtnRightURL ? item.SPFxBtnRightURL : ‘#’}"><div class="slider-button ${styles.sliderButton}" target="_blank"> ${SPFxBtnRightTxt} </div></a>`;
}
//check if slide is the first (active) or not
let activeFlag: string = "";
if (index == 0) {
activeFlag = "active";
}
//return slide HTML
return `
<div class="${styles.item} item ${activeFlag}" style="background-image: url(${item.UrlSlideBGImg ? item.UrlSlideBGImg : ”})">
<div class="row">
<div class="col col-xs-12 col-sm-6 col-md-6 carousel-image ${styles.carouselImage}">
<img src="${item.UrlSlideImg ? item.UrlSlideImg : ”}" alt="${item.Title ? item.Title : ”}">
</div>
<div class="col col-xs-12 col-sm-6 col-md-6 carousel-text ${styles.carouselText}">
<h1>${item.Title ? item.Title : ”}</h1>
<p>${item.SPFxDesc ? item.SPFxDesc : ”}</p>
<div class="slider-buttons ${styles.sliderButtons}">
${leftButtonHTML}
${rightButtonHTML}
</div>
</div>
</div>
</div>`;
}
[/code]

  1. Finally, the big puppy, the render() method

[code language=”javascript”]
public render(): void {

// only render this once
if (!this.renderedOnce) {
var me = this;
// Get users profile object
this._getCurrentUserProfile()
.then((_userprofileresponse: IUserProfileResponse) => {
//console.log("userprofileresponse: ");
//console.log(_userprofileresponse);

// format the users profile to compare to news
let userProfile: IUserProfile = this._userProfile(_userprofileresponse);

//console.log("userProfile: " + userProfile);

//Go get the slider. Compare to users profile object and determine to display it or not

this._getSlides()
.then((response: ISlides): void => {
// get slider images
response.value.forEach((slide: ISlide): void => {
this._getImage(slide.ID)
.then((data: ISlideItem): void => {
//Fix these image queries to be failsafe if nothing comes back for each slide image
//Slider Image
// get the image out of the FieldValuesAsHtml
let divSliderImg = document.createElement(‘div’);
divSliderImg.innerHTML = data.SPFxSliderImage;
let imgSlider: HTMLImageElement = divSliderImg.firstChild as HTMLImageElement;
// need to do string split inorder to make the url relative
const imgSliderUrl: string = imgSlider.src.split(this.context.pageContext.web.serverRelativeUrl)[1];
//Slider BG Image
// get the image out of the FieldValuesAsHtml
let divSliderBGImg = document.createElement(‘div’);
divSliderBGImg.innerHTML = data.SPFxSliderBGImage;
let imgSliderBG: HTMLImageElement = divSliderBGImg.firstChild as HTMLImageElement;
// need to do string split inorder to make the url relative
const imgSliderBGUrl: string = imgSliderBG.src.split(this.context.pageContext.web.serverRelativeUrl)[1];
//Continue to store data from query for each slide
const item: slides = {
ID: slide.ID,
Title: slide.Title,
UrlSlideImg: `${this.context.pageContext.web.absoluteUrl}/_layouts/15/getpreview.ashx?resolution=2&path=${this.context.pageContext.web.serverRelativeUrl}${imgSliderUrl}&clientType=modernWebPart`,
UrlSlideBGImg: `${this.context.pageContext.web.absoluteUrl}/_layouts/15/getpreview.ashx?resolution=2&path=${this.context.pageContext.web.serverRelativeUrl}${imgSliderBGUrl}&clientType=modernWebPart`,
SPFxDesc: slide.SPFxDesc,
SPFxBtnLeftTxt: slide.SPFxBtnLeftTxt,
SPFxBtnLeftURL: slide.SPFxBtnLeftURL,
SPFxBtnRightTxt: slide.SPFxBtnRightTxt,
SPFxBtnRightURL: slide.SPFxBtnRightURL,
SPFxSliderExpire: slide.SPFxSliderExpire,
SPFxOrder: slide.SPFxOrder,
Language: slide.Language,
Region: slide.Region,
Office: slide.Office

};
console.log("Slide ID:" + item.ID);
let slideOfficeString: string = item.Office + ”;

let slideOffice: Array<string> = slideOfficeString.split(","); //["Los Angeles", "Value 1", "Value 2"];
console.log("Slide Office" + slideOffice);
let userOffice: Array<string> = userProfile.Office; //["Los Angeles", "Value 1"];

let slideLanguage: string = item.Language; //"English";
let userLanguage: string = userProfile.Language; //should this support multiple?;
console.log("Slide Language:" + slideLanguage);

let slideRegion: Array<string> = item.Region; //["USA", "United Kingdom", "Latin America"];
let userRegion: string = userProfile.Region;
console.log("Slide Region:" + slideRegion);

// reset each news items flag for users filter

let flag1: boolean = false;
let flag2: boolean = false;
let flag3: boolean = false;

// compare each user variable to slide item column

// see if users Office is in the slider item
// make case upper
for (let i = 0; i < slideOffice.length; i++) {
slideOffice[i] = slideOffice[i].toUpperCase();
}
for (let i = 0; i < userOffice.length; i++) {
userOffice[i] = userOffice[i].toUpperCase();
}
let resultcompare: Array<string> = intersection(userOffice, slideOffice);
if (resultcompare.length > 0) {
flag1 = true;
}

// see if users language is in the slider item
if (slideLanguage.toUpperCase() === userLanguage.toUpperCase()) {
flag2 = true;
}

//See if users region is in the slider item
// FIX- This should be list driven from the region/country list and UPS.
for (let i = 0; i < slideRegion.length; i++) {
slideRegion[i] = slideRegion[i].toUpperCase();
}
if (slideRegion.indexOf(userRegion.toUpperCase()) > -1) {
flag3 = true;
}

console.log("Compare Result- Office: " + flag1);
console.log("Compare Result- Language: " + flag2);
console.log("Compare Result- Region: " + flag3);

// if all 3 filters match, show the news item to the user

if (flag1 && flag2 && flag3) {
// Print IT! Append the item to the News object to be pushed to the DOM!

this._slides.push(item);
//console.log("Print the slider item!");
}
else {
console.log("Skipping Slide " + item.ID + ". There are " + this._slides.length + " slides that have been added.");
}

})
.then((): void => {
this.domElement.innerHTML = `
<div class="carousel-container">
<div style="height: 15px; width: 100%; background: #333333;"></div>

<div id="myCarousel" class="carousel slide ${styles.carousel}" data-ride="carousel">
<!– Indicators –>
<ol class="${styles.carouselIndicators} carousel-indicators"></ol>
<!– Wrapper For Slides –>
<div class="${styles.carouselInner} carousel-inner" role="listbox"></div>
<!– Side Controls –>
<a class="${styles.carouselControl} carousel-control left" href="#myCarousel" data-slide="prev"><span class="glyphicon ${styles.glyphiconChevronLeft} glyphicon glyphicon-chevron-left"></span></a>
<a class="${styles.carouselControl} carousel-control right" href= "#myCarousel" data-slide="next" > <span class="glyphicon ${styles.glyphiconChevronRight} glyphicon glyphicon-chevron-right"></span></a>
</div>
</div>`;

// add news items to domElement
if (this._slides.length > 0) {

//Quick fix for edit mode saving not doubling query to slides
jQuery(‘.carousel-indicators’).empty();
var today = new Date();

// Sort by date again
var sortResults: slides[] = this._slides.sort(function compare(a, b) {
if (a.SPFxOrder > b.SPFxOrder) {
return 1;
}
if (a.SPFxOrder < b.SPFxOrder) {
return -1;
}
return 0;
});
sortResults.forEach((item: slides, index: number): void => {
if (index <= 3 && item.SPFxSliderExpire <= today) {

jQuery(‘.carousel-indicators’, this.domElement).append(this._itemCarouselIndicators(index));
jQuery(‘.carousel-inner’, this.domElement).append(this._itemSlideWrapper(item, index));
console.log("Slide "+index + " ID: " + item.ID + " appended to DOM.");
}
else {
console.log("Skipping Slide " + index + " ID: " + item.ID + " Expires: " + item.SPFxSliderExpire + " " + item.SPFxOrder + " Total Slides in list:" + this._slides.length + " and today’s date is: " + today);
}
});
}
else {
// no slide items case here
}
// initialize the slider
jQuery(‘#myCarousel’).carousel(‘pause’);

});
});
});
});
}
}
[/code]

  1. Add the SASS (I removed some of the colors so it might not be perfect):

[code language=”css”]
$white: #fff;
$black: #000;
$blue: blue;

@import url(‘https://fonts.googleapis.com/css?family=Open+Sans:300,400,600’);

@mixin transition() {
-webkit-transition: 0.5s;
-moz-transition: 0.5s;
-ms-transition: 0.5s;
-o-transition: 0.5s;
transition: 0.5s;
}

.carousel {
background-color: #3279c3;
font-weight: 100;

.item {
min-height: 400px; /* Prevent carousel from being distorted if for some reason image doesn’t load */
background-repeat: no-repeat;
background-size: 45%;

img {
margin: 0 auto; /* Align slide image horizontally center */
}
}

.carouselInner {
.carouselImage {
text-align: center;
}

img {
width: 100%;
max-height: 400px;
max-width: 500px;
padding: 10px;
}

.col {
@media screen and (max-width: 767px) {
text-align: center;
}
}

.carouselText {
max-width: 510px;
color: $white;
font-size: 14px;
font-weight: 300;
line-height: 25px;
letter-spacing: 1px;
padding: 0 30px;

@media screen and (max-width: 767px) {
text-align: center;
max-width: 100%;
}

h1 {
font-size: 28px;
font-weight: 100;

@media screen and (max-width: 767px) {
font-size: 48px;
font-weight: 300;
}
}

p {
margin-bottom: 30px;
color: $white;

@media screen and (max-width: 767px) {
font-size: 12px;
line-height: 18px;
}
}

.sliderButtons {
display: flex;

a {
background: $white;
border: 0;
border-radius: 4px;
font-family: ‘Segoe UI’, Arial, sans-serif;
font-size: 14px;
font-weight: 400;
margin-bottom: 20px;
text-align: center;
text-decoration: none;
text-transform: none;
width: 50%;

@media screen and (max-width: 767px) {
min-width: 100%;
}

&:hover {
background-color: #394a4f;
border: 0;
text-decoration: none;
}
}

.sliderButton {
border: 0;
border-radius: 4px;
color: $blue;
display: block;
font-size: 14px;
font-weight: 400;
letter-spacing: normal;
padding: 6px 10px;
text-align: center;
text-transform: none;

&:hover {
background-color: #394a4f;
border: 0;
color: $white;
}
}
}
}
}

.carouselIndicators {
/*position: relative;
display: inline-block;
width: 100%;
margin-left: 0;
left: 0;
@media screen and (max-width: 767px) {
margin-top: 50px;*/
bottom: 6px;

li {
background: $white;
width: 10px;
height: 10px;
margin: 0 8px;
border: none;
}

li {
.active {
background: #c0c0c0 !important;
border: none;
width: 10px;
height: 10px;
}
}
}

@media screen and (max-width: 767px) {
.carouselIndicators {
display: none;
}
}

.carouselControl .glyphiconChevronLeft, .carouselControl .glyphiconChevronRight, .carouselControl .iconNext, .carouselControl .iconPrev {
width: 40px;
height: 40px;
margin-top: -10px;
font-size: 40px;

@media screen and (max-width: 767px) {
display: none;
}
}
}

[/code]

  1. Now we are ready to build and deploy! Run these node.js commands to package. Then install your app package file and assets. (I deploy mine to CDN, see the SPFX Web Part CDN tutorial on the MS website https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/hosting-webpart-from-office-365-cdn ):

[code language=”powershell”]gulp bundle –ship
gulp package-solution –ship[/code]

  1. Add the app to your site
  2. On the new My List that is created, add the Language site column (OOTB).
  3. Add some list data to your My List and tag data. (below is a similar list I added the site columns to, its not the same for the slider since this list was for news. That’s all I had for sample data sorry)

  1. Add the web part to your modern page:

  1. It will appear here, if not, ctrl+f5 or check for build errors:

  1. View the web part:

  1. Use F12 Chrome to debug web part

Please comment below any enhancements to the code, or big areas I may have messed up on when sanitizing the data.

Thanks!