Please NOTE: This blog series is currently a work in progress and subject to change.

This is part 6 in a series about creating a React App, using the Microsoft PNPJS library and Ui-Fabric in SharePoint.

Disclaimer


WebDev By The Bay is not affiliated with nor is this post endorsed by any company mentioned in this post. 

All opinions belong to the author of this post.

All Copyrights and Trademarks belong to their respective owners.

Full disclosure!

I am not a Software Engineer, I’m a Web Developer of almost 20 years. In my day job, for the past 6 years, I have been tasked with developing solutions in SharePoint.

This series is my journey in learning not only SharePoint but various other technologies.

Please Let me be clear! We will create some custom code but most of the code will be from the library examples to make learning the technology easier, at least for me.

This series is not about showing you how to code the various pieces but rather how you can piece the pieces together to make an application.

The basic setup of the React App is based on react-starter by Ted Pattison.

I will be adding how to use the PNPJS Library to surface SharePoint list data, modifying webpack.config and providing some tips on using the Ui-Fabric.

The app will use the Microsoft PNPJS library to surface the data in the backing SharePoint list data and Microsoft Power Automate to do the Approval workflows.

Please note the following:
1.The PNPJS library may be out of date at the time you read this post.
Please be sure to check for updates.
2. You will need to upload the bundle js file and index.aspx to SharePoint before you can debug communication with SharePoint List data.

App Purpose

Dashboard app to help manage Long Term Parking Requests and Approvals.

Please NOTE: PNPJS v2 was published Dec 20, 2019.
We will use v2 in this tutorial.
https://pnp.github.io/pnpjs/transition-guide/

Stop the project if it is still running. In part 5 we modified the form and used PnPJs to save our form data to our SharePoint list. We also added a toggle button to enable or disable the form controls. In this part we will change the Manager fields to read only We will populate the controls, using the Microsoft Graph to return data from Azure Active Directory.

WebDev By The Bay - Parking Request Toggle Button
WebDev By The Bay – Parking Request Toggle Button

Why not create a SharePoint Framework solution?

The SharePoint Framework is a great option and is the only app model that Microsoft is recommending going forward.

SPFx is not limited to just SharePoint, it can be the model for creating Teams tabs and apps.

Productivity!

You can write your code and test on your local laptop, the SPFX installs a dashboard to test your app. You can also test your app in the SharePoint Online Dashboard, make your changes on your laptop and test in an actual SharePoint instance without having to re-upload your app.

I stated this in the overview, I’m adding a little more to this subject because of the importance in determining which app model we choose. Just as with Provider or SharePoint Hosted apps, any web part you create with SPFx must be added to the SharePoint App Catalog by someone with the permission to do so.

Also all changes to your SPFx web parts must be updated in the SharePoint App Catalog by someone with the permission to do so. I am a department developer and owner of several SharePoint sites but I do not have the permission to add apps to the SharePoint App Catalog.

In my case any and all apps that need to be added to the SharePoint App Catalog must be pre approved, this could involve filing a formal declaration with your IT and Legal Departments. This declaration may cover the purpose of your app, what company systems it will interact with, what 3rd party systems will your app interact with, do NDA’s, Contracts need to be considered, who will be responsible for long term support, what types of data will the app surface, where will the data be stored, what security measures are involved with the data, will PII(Personally Identifiable Information) be involved, what 3rd party libraries are used in your app etc…

Once the request is submitted it may be determined that your app needs to go through a formal review, this is where you present your app and all documentation before a panel of Engineers, Managers and possibly Lawyers. You may be required to walk through your app with them, demo the app. Then the panel decides if your project should be allowed to proceed and if any changes are required.

This is necessary to insure that your app does not cause harm to your enterprises operations, or subjects the Enterprise to Legal issues. This process could take anywhere from 2 weeks to over a year before you can even start developing. Even changes and upgrades may require further formal declarations.

In a lot of scenarios this is overkill but since my app may require assistance from the IT department to get it installed, those procedures may be my only option.

Power Apps Embedded app Pros and Cons?

Pros –

In my opinion, Power Apps is a great choice for Stand Alone apps, SharePoint List Forms or for apps embedded in a SharePoint Modern Page.

Cons –

Also in my opinion, Power Apps are not ideal for embedding in an app that is not surfaced in any of the above scenarios. Power Apps are not ideal for embedding in a SharePoint Folder solution or embedding in a normal web page.

Even though the Office365Users connector makes looking up employee information incredibly simple, there is an undesirable scenario where Power Apps that are not surfaced from a SharePoint Modern Page, is that Power Apps are not context aware if being surfaced in a normal html file.

The only thing I can think of is that, and this is just me thinking out loud, is that an Embedded Power App does not take into account that the users Network and SharePoint sessions are currently valid due to our app, since it is an html file hosted in a SharePoint folder, our app is not actually in the context of SharePoint, it is just making calls to SharePoint but it is outside the scope of SharePoint.

At various times the Power App may require the user to re-authenticate. The issue is that the user needs to re-authenticate to Power Apps not the host, which in this scenario the host is our app hosted in a SharePoint folder.

This has been very discerning for my co-workers. I would get questions about why am I being asked to login again, why do I have to login through the Microsoft Login Dialog and not our company login? We are already logged in to our internal Network and authenticated to access SharePoint, why is this happening?

You may think this is a minor inconvenience but with an employee base that goes into the tens of thousands, it actually becomes a huge inconvenience.

SharePoint folder solution Pros and Cons?

Pros –

  • Does not require SharePoint Admin permissions
  • Does not require your solution be added to the SharePoint App Catalog!
  • Your solution is isolated to your tenant

Cons – Lost Productivity!

You cannot surface data from SharePoint on your local machine, unless you setup your app as a Provider Hosted app and your app is registered in the SharePoint App Catalog. If not registered, you will need to upload your app into the SharePoint folder and use the browser developer tools to debug.

When you debug in the browser using the developer tools, in some scenarios your app may trigger an exception where the page will show the SharePoint Blue Screen or your SharePoint folder structure instead of your page data.

SharePoint Blue Screen
SharePoint Blue Screen

In either case, at this point your only option may be to close the offending page, clear your browser cache, reload the screen that shows your apps index.aspx page and then you may be required to log back into SharePoint, delete the Index.aspx page and the bundle js file. Then make code changes on your local machine, Re-Build, and upload the Index.aspx and bundle files and start over. Not Ideal!

Another thing to consider is that your app may break when updates to SharePoint are rolled out. The only model that Microsoft guarantees not to break with changes to SharePoint are apps developed as SharePoint Framework solutions.

Why Do We Need The Graph?

You may ask, you don’t want to use an embedded Power App but now you’re talking about using the Graph which requires your co-workers to login with the “Microsoft Login” also. Isn’t this the same scenario? Short answer is NO.

I’ve only had the login associated with the Graph occur once per session, it does not time out (well actually it does time out but it uses a refresh token and the user won’t be asked to login again unless they close the browser window), and ask the user to re-authenticate like an embedded Power App does after the Power App Idle period times out, which occurs at random times from what I have experienced.

Why do we need to use the Microsoft Graph, we are already using SharePoint?

Why can’t we just use the SharePoint People Picker control to surface employee information?

In a SharePoint folder App scenario, the SharePoint People Picker control is not available in this context. We need to create our own control as we did in returning the current user by calling the pnp/sp function web.currentUser()

The SharePoint People Picker control and web.currentUser() only surface employee information for employee’s who have a SharePoint Profile.

Ok, So?

There may be a scenario in your company where some employees do not require access to your company network resources due to their job responsibilities, i.e. Janitors, Construction Workers, Delivery, even some Temp Agency workers etc… Therefore your IT department may choose to not create a SharePoint Profile for these individuals. The People Picker control and web.currentUser() would not be able to surface their information.

However, all personnel who require a company issued employee ID Badge must have an Active Directory account, from my experience, your experience may be different. This post assumes you or your company is using Azure Active Directory.

AD is usually the main source of all employee information that can be accessed by internal departments in a secured manner within an organization.

AD information is usually a subset of information provided by an HR system such as PeopleSoft or Workday. These HR systems are restricted and not all information is generally made available to access through an API or app by just anyone in the organization. Active Directory usually contains a subset of information from these HR systems.

The Graph makes it easier to access employee information from Active Directory. We can also use the Graph to connect with Office apps, Planner, Outlook, Yammer, Teams, SharePoint and a lot more. Please see the Microsoft Graph Overview to find out what sources you can surface information from.

Getting Started!

There are several approaches we could take to interact with the Microsoft Graph, here are a few.

Microsoft Graph API: https://docs.microsoft.com/en-us/graph/use-the-api

Microsoft Graph SDKs: https://docs.microsoft.com/en-us/graph/sdks/sdks-overview

Microsoft Graph JavaScript Client Library: https://www.npmjs.com/package/@microsoft/microsoft-graph-client
or from github: https://github.com/microsoftgraph/msgraph-sdk-javascript

Microsoft Graph Toolkit: https://docs.microsoft.com/en-us/graph/toolkit/overview

PnPJs Graph package: https://pnp.github.io/pnpjs/graph/

Even though the Toolkit is the easiest to implement in my opinion, it must be run in a web server. SharePoint is not a web server.

If using the API you will need to provide your own way of handling the Bearer and Refresh Tokens.

My first approach was to use the PnPJs Graph package but I ran into several issues with this package that I will need to follow up on later.

In this post I will be using the Microsoft Graph JavaScript Client Library. This library handles the Bearer and Refresh tokens for us.

Please refer to: https://github.com/microsoftgraph/msgraph-sdk-javascript

MSAL must be installed separately : https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-core#installation

App Setup!

Register your app with Azure Active Directory to get the ClientID (aka. AppID) and TenantID (aka DirectoryID).

On the command line in the same directory as the projects package.json file issue the following commands:

npm install @microsoft/microsoft-graph-client
npm install msal

// This may or may not be required
npm install --save isomorphic-fetch es6-promise

// and if not already installed
npm install office-ui-fabric-react

// Intellisense
npm install @microsoft/microsoft-graph-types --save-dev

The package.json should now look like the following:

{
  "name": "safepark",
  "version": "1.0.0",
  "description": "Parking Request Manager",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server --open --history-api-fallback"
  },
  "keywords": [],
  "author": "Keith Craigo",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.7.7",
    "@babel/preset-env": "^7.7.7",
    "@microsoft/microsoft-graph-types": "^1.12.0",
    "@types/es6-promise": "^3.3.0",
    "@types/node": "^10.17.11",
    "@types/react": "^16.9.17",
    "@types/react-dom": "^16.9.4",
    "@types/react-router": "^5.1.3",
    "@types/react-router-dom": "^5.1.3",
    "awesome-typescript-loader": "^5.2.0",
    "clean-webpack-plugin": "^0.1.19",
    "copy-webpack-plugin": "^4.6.0",
    "css-loader": "^0.28.11",
    "eslint": "^6.8.0",
    "eslint-plugin-react": "^7.17.0",
    "expose-loader": "^0.7.5",
    "file-loader": "^1.1.11",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.9.3",
    "office-ui-fabric-react": "^6.211.0",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-hooks": "^1.0.1",
    "react-router": "^5.0.1",
    "react-router-dom": "^5.1.2",
    "sass-loader": "^7.3.1",
    "style-loader": "^0.21.0",
    "typescript": "^3.7.4",
    "url-loader": "^1.1.2",
    "webpack": "^4.41.4",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1",
    "msal": "^1.2.1"
  },
  "dependencies": {
    "@babel/polyfill": "7.6.0",
    "@microsoft/sp-core-library": "1.9.1",
    "@microsoft/sp-lodash-subset": "^1.9.1",
    "@pnp/common": "^2.0.3",
    "@pnp/graph": "^2.0.3",
    "@pnp/logging": "^2.0.3",
    "@pnp/odata": "^2.0.3",
    "@pnp/sp": "^2.0.0",
    "@types/es6-promise": "0.0.33",
    "@uifabric/example-data": "^7.0.2",
    "@uifabric/react-hooks": "^7.0.1",
    "babel-loader": "^8.0.6",
    "es6-promise": "^4.2.8",
    "eslint-plugin-react-hooks": "^2.3.0",
    "isomorphic-fetch": "^2.2.1",
    "node-sass": "4.13.0",
    "popper.js": "1.16.0",
    "prop-types": "15.7.2",
    "query-string": "^6.8.3",
    "querystringify": "^2.1.1",
    "react-app-polyfill": "1.0.4"
  }
}

Create two new files in the utility folder:

  • config.ts
  • graphService.ts

config.ts

export default function config()
{
    const baseURL = '[YOUR SHAREPOINT INSTANCE URL]';

// Found in the Azure Panel Overview Blade of your app's registration
    const appID = ‘[YOUR APP ID]’; // aka CLIENTID
    const tenant = '[YOUR TENANT]’ // aka DIRECTORYID
    const tenantID ='[YOUR TENANT ID]’; 
    const authority = 'https://login.microsoftonline.com/'+tenantID+'/oauth2/v2.0/authorize?';
    const responseType =  'code';
    const responseMode = 'query';
    const state = '12345';

    const redirectUri ='[YOUR REDIRECT URI]’;

    const scopes = [
                        'user.read',
                        'calendars.read'
                    ];

    return {appID: appID, baseURL: baseURL, redirectUri: redirectUri, scopes: scopes, tenant: tenant, tenantID: tenantID, authority: authority, responseMode: responseMode, responseType: responseType, state: state};

}

graphService.ts

// import "isomorphic-fetch";
import { Client } from "@microsoft/microsoft-graph-client";
// import * as Msal from "msal";

import { UserAgentApplication } from "msal";

import { ImplicitMSALAuthenticationProvider } from "../../node_modules/@microsoft/microsoft-graph-client/lib/src/ImplicitMSALAuthenticationProvider";

import { MSALAuthenticationProviderOptions } from "../../node_modules/@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions";

import config from "./config";


// An Optional options for initializing the MSAL @see https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/MSAL-basics#configuration-options
const msalConfig = {
  auth: {
    clientId: config().appID, // Client Id of the registered application
    redirectUri: config().redirectUri
  }
};
const graphScopes = config().scopes; // An array of graph scopes

// Important Note: This library implements loginPopup and acquireTokenPopup flow, remember this while initializing the msal
// Initialize the MSAL @see https://github.com/AzureAD/microsoft-authentication-library-for-js#1-instantiate-the-useragentapplication
const msalApplication = new UserAgentApplication(msalConfig);
const options = new MSALAuthenticationProviderOptions(graphScopes);
const authProvider = new ImplicitMSALAuthenticationProvider(
  msalApplication,
  options
);
const authoptions = {
  authProvider // An instance created from previous step
};

const client = Client.initWithMiddleware(authoptions);

export async function graphAuthService(cEmp) {
  try {
    const userDetails = await client.api("/users/" + cEmp).get();

    return userDetails;
  } catch (error) {
    throw error;
  }
}

export async function getManager(cEmp) {
  try {
    const managerDetails = await client
      .api("/users/" + cEmp + "/manager")
      .get();

    console.log("GRPHSVC-MANAGER DETAILS: " + managerDetails.displayName);

    return managerDetails;
  } catch (error) {
    throw error;
  }
}

Modify user.tsx

import * as React from 'react';
import { useState } from 'react';

import { Web } from "@pnp/sp/presets/all";
import config from "./config";

export default async function appUser()
{
    const web = Web(config().baseURL);
    try {
          const cUser = await web.currentUser();
          console.log("UserPrincipalName: " + cUser.UserPrincipalName);
          return cUser.UserPrincipalName;
          } catch (error) {return error}

}

If you have not already done so, open index.html and remove

<link rel="stylesheet"
        href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/10.0.0/css/fabric.min.css" />

Also If you have not already done so, in App.scss add the following import

@import '~office-ui-fabric-react/dist/sass/_References.scss';

Let’s change requestForm.tsx to accommodate the following:

  • Call the Microsoft Graph to return the current user, aka. Initiator, information.
  • Call the Microsoft Graph to return the current users Manager information.
  • Add a Text Box to allow the user to request information on behalf of another employee by their First Name.
  • Make the separation of these areas more clear by placing them into their sections on the form.

A quick note about the Login.

When the user first navigates to the Request Form, if the user is not currently logged into an O365 application such as SharePoint Online, Outlook Online, Excel Online etc… , they will be shown the Microsoft Login dialog box below. If they are currently logged into O365, this dialog will still appear but it may not require any user interaction. This is SSO (Single Sign On) within O365.

Depending on the options you selected when you registered your app, the user will be able to select which account they wish to login with.

Microsoft Login Dialog

They may also be asked to approve any requested permissions.

This is the consent dialog for an app called wp-blog-manager, this would be replaced with safepark in this case.

Change the requestForm.tsx to match the following:

import * as React from "react";
import { useState } from "react";

import { Separator } from "office-ui-fabric-react/lib/Separator";
import { Text } from "office-ui-fabric-react/lib/Text";

import {
  DatePicker,
  DayOfWeek,
  Dropdown,
  IDatePickerStrings,
  IDropdownOption,
  Label,
  Link,
  MaskedTextField,
  mergeStyles,
  mergeStyleSets,
  MessageBar,
  MessageBarType,
  PrimaryButton,
  TextField,
  Toggle,
  DefaultButton,
  Button
} from "office-ui-fabric-react";
import { Icon } from "office-ui-fabric-react/lib/Icon";
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";

import { useEffect, useReducer } from "react";
import IBuilding from "../../../models/IBuilding";
import ICampus from "../../../models/ICampus";
import "./requestForm.scss";

import appUser from "../../../utility/user";

import { sp } from "@pnp/sp/presets/all";
import { IItemAddResult } from "@pnp/sp/items";

import { graphAuthService, getManager } from "../../../utility/graphService";
import config from "../../../utility/config";
import { JSONParser } from "@pnp/odata";

// import "isomorphic-fetch";

const PolicyIcon = () => (
  <Icon iconName="DocumentSet" className="ms-IconExample" />
);

const iconClass = mergeStyles({
  fontSize: 50,
  height: 50,
  width: 50,
  margin: "0 25px"
});

const classNames = mergeStyleSets({
  deepSkyBlue: [{ color: "deepskyblue" }, iconClass],
  greenYellow: [{ color: "greenyellow" }, iconClass],
  salmon: [{ color: "salmon" }, iconClass]
});

const DayPickerStrings: IDatePickerStrings = {
  months: [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December"
  ],

  shortMonths: [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec"
  ],

  days: [
    "Sunday",
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday"
  ],

  shortDays: ["S", "M", "T", "W", "T", "F", "S"],

  goToToday: "Go to today",
  prevMonthAriaLabel: "Go to previous month",
  nextMonthAriaLabel: "Go to next month",
  prevYearAriaLabel: "Go to previous year",
  nextYearAriaLabel: "Go to next year",
  closeButtonAriaLabel: "Close date picker"
};

const controlClass = mergeStyleSets({
  control: {
    margin: "0 0 15px 0",
    maxWidth: "300px"
  }
});

export interface IDatePickerState {
  firstDayOfWeek?: DayOfWeek;
}

export interface ICompanyLocationState {
  campuses: ICampus[];
  buildings: IBuilding[];
  selectedCampus: string;
}

export interface ICampuses {
  Id: number;
  Title: string;
}

export interface IBuildings {
  Id: number;
  Title: string;
  Street: string;
  City: string;
  PostalCode: string;
}

const levels: IDropdownOption[] = [
  { key: "ground", text: "Ground" },
  { key: "1", text: "1" },
  { key: "2", text: "2" },
  { key: "3", text: "3" },
  { key: "4", text: "4" },
  { key: "5", text: "5" }
];

let selectedEndDate: Date;
let selectedStartDate: Date;
let userDetails: any;
let managerDetails: any;

// Initialize
let businessPhones = "";
let displayName = "";
let givenName = "";
let jobTitle = "";
let mail = "";
let mobilePhone = "";
let officeLocation = "";
let prefferedLanguage = "";
let surname = "";
let userPrincipalName = "";
let cUser = "";

const stackTokens: IStackTokens = { childrenGap: 12 };
const HorizontalSeparatorStack = (props: { children: JSX.Element[] }) => (
  <>
    {React.Children.map(props.children, child => {
      return <Stack tokens={stackTokens}>{child}</Stack>;
    })}
  </>
);

export const ParkingRequestForm = () => {
  sp.setup({
    sp: {
      headers: {
        Accept: "application/json;odata=verbose"
      },
      baseUrl: config().baseURL
    }
  });

  const [state, dispatch] = useReducer(reducer, []);
  const [isDisabled, toggleDisable] = useState(true);

  let [selectedBuilding, setBuilding] = useState("");
  let [selectedLevel, setLevel] = useState("");
  let [selectedCampus, setCampus] = useState("");

  // Initiator User Details
  let [initiatorName, setInitiator] = useState("");
  let [initiatorEmail, setInitiatorEmail] = useState("");
  let [initiatorJobTitle, setInitiatorJobTitle] = useState("");
  let [initiatorPhone, setInitiatorPhone] = useState("");

  //   // Manager
  let [managerName, setManagerName] = useState("");
  let [managerEmail, setManagerEmail] = useState("");
  let [managerJobTitle, setManagerJobTitle] = useState("");
  let [managerPhone, setManagerPhone] = useState("");
  let [noManager, setNoManager] = useState(true); // false = visible, true = hidden

  // Search Employee Details
  let [searchUserName, setSearchUser] = useState("");
  let [searchUserEmail, setSearchUserEmail] = useState("");
  let [searchUserJobTitle, setSearchUserJobTitle] = useState("");
  let [searchUserPhone, setSearchUserPhone] = useState("");
  let [validEmployee, setValidEmployee] = useState(true); // false = visible, true = hidden

  const initiator = "Initiator Information";
  const manager = "Manager Information";
  const employee = "Employee Information";
  const vehicle = "Vehicle Information";
  const emergencyContact = "Emergency Contact Information";
  const parking = "Parking Information";
  //   const cUser = appUser();

  useEffect(() => {
    _getInitiator();
    return () => {
      console.log("Init - INITIATOR");
    };
  }, []);

  useEffect(() => {
    _getCampuses();
    return () => {
      console.log("Init - CAMPUSES");
    };
  }, []);

  return (
    <form className="requestForm" name="parkingRequestForm">
      <div className="container">
        <div className="ms-Grid" dir="ltr">
          <div className="ms-Grid-row">
            <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg4">
              <Stack
                horizontal
                tokens={{ childrenGap: 50 }}
                styles={{ root: { width: 850 } }}
              >
                <Link className="policyLink" href="#/policies">
                  <PolicyIcon />
                  <strong>Parking Policies</strong>
                </Link>
                <Toggle
                  id="IhaveReadParkingPolicy"
                  onText="I have read and understand the Parking Policy."
                  offText="Please make sure you have read and understand the Parking Policy!"
                  onChange={() => toggleDisable(!isDisabled)}
                />
              </Stack>
            </div>
            <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
              <Text>
                <strong>All Fields are Required!</strong>
              </Text>

              <MessageBar>
                A Mobile App version of this form is available from the Power Apps Viewer. The
                viewer can be downloaded from the App Store link on your mobile device.
              </MessageBar>
            </div>
          </div>
          <div className="ms-Grid-row row-spacer">
            <>
              <Separator>{initiator}</Separator>
            </>

            <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg2">
              <div className="ms-Grid-row row-spacer">
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg2">
                  <Stack
                    horizontal
                    tokens={{ childrenGap: 50 }}
                    styles={{ root: { width: 850 } }}
                  >
                    <Stack>
                      <Label>Employee:</Label>
                      <Label id="initiatorName">{initiatorName}</Label>
                    </Stack>
                    <Stack>
                      <Label>Email:</Label>
                      <Label id="initiatorEmail">{initiatorEmail}</Label>
                    </Stack>
                    <Stack>
                      <Label>Title:</Label>
                      <Label id="initiatorJobTitle">{initiatorJobTitle}</Label>
                    </Stack>
                    <Stack>
                      <Label>Phone:</Label>
                      <Label id="initiatorPhone">{initiatorPhone}</Label>
                    </Stack>
                    <TextField
                      id="initiatorEmpID"
                      label="EmployeeID"
                      disabled={isDisabled}
                    />
                  </Stack>
                </div>
              </div>
            </div>
          </div>

          <div className="ms-Grid-row row-spacer">
            <>
              <Separator>{employee}</Separator>
            </>
            <div className="ms-Grid-row row-spacer">
              <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg2">
                <Stack
                  horizontal
                  tokens={{ childrenGap: 50 }}
                  styles={{ root: { width: 850 } }}
                >
                  <TextField
                    id="searchByEmployeeName"
                    label="Employee First Name"
                    disabled={isDisabled}
                  />
                  <Stack>
                    <Label>Employee:</Label>
                    <Label id="searchUserEmail">{searchUserName}</Label>
                  </Stack>
                  <Stack>
                    <Label>Title</Label>
                    <Label id="searchUserTitle">{searchUserJobTitle}</Label>
                  </Stack>
                  <Stack>
                    <Label>Phone</Label>
                    <Label id="searchUserPhone">{searchUserPhone}</Label>
                  </Stack>
                  <TextField
                    id="searchEmpID"
                    label="EmployeeID"
                    disabled={isDisabled}
                  />
                </Stack>
                <div hidden={validEmployee}>
                  <MessageBar messageBarType={MessageBarType.error}>
                    This employee does not exist!
                  </MessageBar>
                </div>
              </div>
            </div>
          </div>
          <div className="ms-Grid-row row-spacer">
            <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg4">
              <PrimaryButton
                className="deepSkyBlue"
                text="SEARCH"
                allowDisabledFocus
                onClick={_searchUser}
                disabled={isDisabled}
              />
            </div>
          </div>

          <div className="ms-Grid-row row-spacer">
            <>
              <Separator>{manager}</Separator>
            </>
            <div hidden={noManager}>
              <MessageBar messageBarType={MessageBarType.warning}>
                CEO does not have a manager.
              </MessageBar>
            </div>
            <div className="ms-Grid-row row-spacer">
              <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg2">
                <Stack
                  horizontal
                  tokens={{ childrenGap: 50 }}
                  styles={{ root: { width: 850 } }}
                >
                   <Stack>
                    <Label>Name:</Label>
                    <Label id="managerName">{managerName}</Label>
                  </Stack>
                   <Stack>
                    <Label>Email:</Label>
                    <Label id="managerEmail">{managerEmail}</Label>
                  </Stack>
                  <Stack>
                    <Label>Title:</Label>
                    <Label id="managerJobTitle">{managerJobTitle}</Label>
                  </Stack>
                  <Stack>
                    <Label>Phone:</Label>
                    <Label id="managerPhone">{managerPhone}</Label>
                  </Stack>
                </Stack>
              </div>
            </div>
          </div>

          <div className="ms-Grid-row row-spacer">
            <>
              <Separator>{emergencyContact}</Separator>
            </>
            <Stack
              horizontal
              tokens={{ childrenGap: 50 }}
              styles={{ root: { width: 650 } }}
            >
              <TextField
                id="contactName"
                label="Contact Name"
                disabled={isDisabled}
              />
              <MaskedTextField
                id="contactPhone"
                label="Phone"
                mask="(999) 999 - 9999"
                disabled={isDisabled}
              />
              <TextField
                id="contactEmailAddress"
                label="Email"
                disabled={isDisabled}
              />
            </Stack>
          </div>
          <div className="ms-Grid-row row-spacer">
            <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg6">
              <Label>Start Date</Label>
              <DatePicker
                id="startDate"
                onSelectDate={_onSelectStartDate}
                disabled={isDisabled}
                className={controlClass.control}
                strings={DayPickerStrings}
                placeholder="Start Date"
                ariaLabel="Start Date"
              />
            </div>
            <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg6">
              <Label>Return Date</Label>
              <DatePicker
                id="returnDate"
                onSelectDate={_onSelectReturnDate}
                disabled={isDisabled}
                className={controlClass.control}
                strings={DayPickerStrings}
                placeholder="Return Date"
                ariaLabel="Return Date"
              />
            </div>
          </div>
        </div>
        <div className="ms-Grid-row row-spacer">
          <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
            <>
              <Separator>{vehicle}</Separator>
            </>
            <Stack
              horizontal
              tokens={{ childrenGap: 50 }}
              styles={{ root: { width: 650 } }}
            >
              <TextField id="make" label="Make" disabled={isDisabled} />
              <TextField id="model" label="Model" disabled={isDisabled} />
              <TextField id="year" label="Year" disabled={isDisabled} />
              <TextField id="color" label="Color" disabled={isDisabled} />
              <TextField
                id="licensePlateNumber"
                label="License Plate"
                disabled={isDisabled}
              />
            </Stack>
            <TextField
              id="distinquishingFeatures"
              label="Distinguishing features and or marks to assist Security personnel in identifying your vehicle."
              multiline
              rows={3}
              disabled={isDisabled}
            />
          </div>
        </div>
        <div className="ms-Grid-row row-spacer">
          <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
            <>
              <Separator>{parking}</Separator>
            </>

            <Stack
              horizontal
              tokens={{ childrenGap: 50 }}
              styles={{ root: { width: 650 } }}
            >
              <Dropdown
                id="campus"
                placeholder="Select a Campus"
                disabled={isDisabled}
                label="Campus"
                defaultSelectedKey=""
                options={state.campuses}
                onChange={_getBuildings}
              />

              <Dropdown
                id="building"
                placeholder="Select a Building"
                disabled={isDisabled}
                label="Building"
                defaultSelectedKey=""
                options={state.buildings}
                onChange={_setBuilding}
              />

              <Dropdown
                id="level"
                placeholder="Level"
                disabled={isDisabled}
                label="Level"
                defaultSelectedKey="Ground"
                options={levels}
                onChange={_setLevel}
              />
            </Stack>
          </div>
        </div>
        <div className="ms-Grid-row row-spacer">
          <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
            <TextField
              id="justification"
              label="Justification"
              multiline
              rows={4}
              disabled={isDisabled}
            />
          </div>
        </div>

        <div className="ms-Grid-row align-right">
          <PrimaryButton
            className="deepSkyBlue"
            text="Submit Parking Request"
            onClick={_saveParkingRequest}
            allowDisabledFocus
            disabled={isDisabled}
          />
        </div>
      </div>
    </form>
  );

  // USER
  async function _getInitiator(): Promise<void> {
    appUser().then(res => {
      // cUser = "[email protected][YOUR COMPANY].onmicrosoft.com";
      cUser = res;
      if (cUser) {
        let searchUser = 0;// 0 or false = visible
        _getUsersGraphInfo(cUser, searchUser);
      }
    });
  }

  function _searchUser() {
    let searchUser = 1;// NOT 0 or false = hidden
    const form = document.forms["parkingRequestForm"];
    let empFirstName = form.elements.searchByEmployeeName.value;
    let empUserPrincipalName = empFirstName + "@[YOUR COMPANY].onmicrosoft.com";
    _getUsersGraphInfo(empUserPrincipalName, searchUser);
  }

  // MICROSOFT GRAPH FUNCTIONS

  async function _getUsersGraphInfo(emp, searchUser): Promise<void> {
    // console.log(
    //   "GRPHSVC-USER DETAILS: " + initiator + " Passed: " + initiator
    // );
    await graphAuthService(emp)
      .then(response => {
        userDetails = response;
        businessPhones = userDetails.businessPhones;
        displayName = userDetails.displayName;
        givenName = userDetails.givenName;
        jobTitle = userDetails.jobTitle;
        mail = userDetails.mail;
        mobilePhone = userDetails.mobilePhone;
        officeLocation = userDetails.officeLocation;
        prefferedLanguage = userDetails.prefferedLanguage;
        surname = userDetails.surname;
        userPrincipalName = userDetails.userPrincipalName;

        if (searchUser !== 1) {
          setInitiator(response.displayName);
          setInitiatorEmail(response.mail);
          setInitiatorJobTitle(response.jobTitle);
          setInitiatorPhone(response.businessPhones[0]);
        } else {
          setSearchUser(response.displayName);
          setSearchUserEmail(response.mail);
          setSearchUserJobTitle(response.jobTitle);
          setSearchUserPhone(response.businessPhones[0]);
          setValidEmployee(true);
        }
      })
      .catch(function(error) {
        if (error.code === "Request_ResourceNotFound") {
          console.log("Response Error: " + error.code);

          //employee
          setSearchUser("");
          setSearchUserEmail("");
          setSearchUserJobTitle("");
          setSearchUserPhone("");
          setValidEmployee(false);
          //manager
          setManagerName("");
          setManagerEmail("");
          setManagerJobTitle("");
          setManagerPhone("");
          throw new Error("Something went badly wrong!");
        }
      });

    if (
      userDetails.jobTitle === "CEO" ||
      userDetails.jobTitle === "" ||
      userDetails.jobTitle === undefined
    ) {
      setNoManager(false);
      setManagerName("");
      setManagerEmail("");
      setManagerJobTitle("");
      setManagerPhone("");
      return;
    } else {
      setNoManager(true);
      _getUsersManagerGraphInfo(emp);
    }
  }

  // Manager
  async function _getUsersManagerGraphInfo(emp): Promise<void> {
    console.log("Call the Graph - Get Manager Information " + emp);
    await getManager(emp).then(response => {
      setManagerName(response.displayName);
      setManagerEmail(response.mail);
      setManagerJobTitle(response.jobTitle);
      setManagerPhone(response.businessPhones[0]);
    });
  }

  // CAMPUSES
  async function _getCampuses(): Promise<void> {
    await sp.web.lists
      .getByTitle("Campus")
      .items.select("Id", "Title")
      .getAll()
      .then(response => {
        const cmps = response.map(desc => {
          return {
            ...desc
          };
        });

        dispatch({
          type: "setCampuses",
          data: cmps
        });
      });
  }

  //BUILDINGS
  async function _getBuildings(
    event: React.FormEvent<HTMLDivElement>,
    campus: IDropdownOption
  ): Promise<void> {
    setCampus((selectedCampus = campus.text));
    await sp.web.lists
      .getByTitle("Buildings")
      .items.select("Id", "Title", "Campus")
      .filter("Campus eq %27" + campus.text + "%27")
      .getAll()
      .then(response => {
        const bldgs = response.map(desc => {
          return {
            ...desc
          };
        });

        dispatch({
          type: "setBuildings",
          data: bldgs
        });
      });
  }

  // FUNCTIONS

  async function _setBuilding(
    event: React.FormEvent<HTMLDivElement>,
    building: IDropdownOption
  ): Promise<void> {
    setBuilding((selectedBuilding = building.text));
  }

  async function _setLevel(
    event: React.FormEvent<HTMLDivElement>,
    level: IDropdownOption
  ): Promise<void> {
    setLevel((selectedLevel = level.text));
  }

  // DATEPICKER
  function _onSelectStartDate(date: Date | null | undefined): void {
    console.log("STARTDATE: " + date);
    selectedStartDate = date;
  }

  function _onSelectReturnDate(date: Date | null | undefined): void {
    console.log("ENDDATE: " + date);
    selectedEndDate = date;
  }

  // SAVE
  async function _saveParkingRequest(): Promise<void> {
    // We need to reference the form in order to gain access to it's controls and the controls values.
    const form = document.forms["parkingRequestForm"];
    // add an item to the list
    // Format - {SharePoint List Field Name}:{Value}

    const iar: IItemAddResult = await sp.web.lists
      .getByTitle("ParkingRequest")
      .items.add({
        Title: "New Parking Request",
        ReadParkingPolicy: true,
        Justification: form.elements.justification.value,
        LicensePlate: form.elements.licensePlateNumber.value,
        Make: form.elements.make.value,
        Color: form.elements.color.value,
        Model: form.elements.model.value,
        Year: form.elements.year.value,
        StartDate: selectedStartDate,
        EndDate: selectedEndDate,
        ContactName: form.elements.contactName.value,
        ContactNumber: form.elements.contactPhone.value,
        ContactEmail: form.elements.contactEmailAddress.value,
        Level: selectedLevel,
        Campus: selectedCampus,
        Building: selectedBuilding,
        Manager: managerName,
        ManagerPhone: managerPhone,
        ManagerEmail: managerEmail,
        Initiator: initiatorName,
        InitiatorPhone: initiatorPhone,
        InitiatorEmail: initiatorEmail,
        Employee: searchUserName,
        EmployeePhone: searchUserPhone,
        EmployeeEmail: searchUserEmail
      });
    console.log("iar: " + iar);
  }
};

export default ParkingRequestForm;

function reducer(state: any, action: { type: any; data: any[] }) {
  switch (action.type) {
    case "setCampuses": {
      // We need to rename the keys: Id to key and Title to text
      return {
        campuses: action.data.map(function(cm: { [x: string]: any }) {
          cm["key"] = cm["Id"];
          delete cm["Id"];
          cm["text"] = cm["Title"];
          delete cm["Title"];
          return cm;
        })
      };
    }
    case "setBuildings": {
      // We need to rename the keys: Id to key and Title to text and keep the current state
      return {
        ...state,
        buildings: action.data.map(function(bldg: { [x: string]: any }) {
          bldg["key"] = bldg["Id"];
          delete bldg["Id"];
          bldg["text"] = bldg["Title"];
          delete bldg["Title"];
          return bldg;
        })
      };
    }

    case "enableFormControls": {
      return { ...state, ...{ isDisabled: false } };
    }

    case "disableFormControls": {
      return { ...state, ...{ isDisabled: true } };
    }

    default:
      return state;
  }
}

The form should now look similar to the following.

Parking Request Form

A quick note about Searching for Employee Information!

In this form I am only searching by the UserPrincipalName. To keep things simple I am only setting this form up to accept the First Name, then I will append the @mycompany.com onto the first name to generate the UserPrincipalName.

You may have also noticed that I added another Message Box to the form to inform the user, never liked the term user, maybe this should be changed to viewer, submitter, requestor, initiator or form filler outer, sorry I digress.

Anyways the 2nd Message Box is in place to inform that if searching for the CEO, the CEO does not have a manager.

Now all that is needed is to modify the backing list to match the form fields, I’ll leave that up to you.

I hope you found this post useful!

References