SPFx Invitation Manager inviting external users through Graph API into the Azure Active Directory (#263)

* first commit

* some corrections

* added textfield to pass the parameters dinamically

* implemented list to display external users

* changing the UI style

* fixed bug on the detail list

* label name modified

* Add files via upload

* Delete upcoming-meetings-preview.png

* Update README.md

* some changes

* added twitter and blog
This commit is contained in:
Giuliano De Luca 2017-07-21 19:59:17 +02:00 committed by Vesa Juvonen
parent 5c8f6a644d
commit 54c391cecd
36 changed files with 10081 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1 @@
* text=auto

View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,14 @@
# Folders
.vscode
coverage
node_modules
sharepoint
src
temp
# Files
*.csproj
.git*
.yo-rc.json
gulpfile.js
tsconfig.json

View File

@ -0,0 +1,3 @@
// Place your settings in this file to overwrite default and user settings.
{
}

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"libraryName": "react-aad-implicitflow",
"framework": "react",
"version": "1.0.0",
"libraryId": "7d08f6e8-b222-4ed7-8910-8ea911d117fe"
}
}

View File

@ -0,0 +1,71 @@
# Azure Active Directory invitation manager Graph API samples
## Summary
Sample SharePoint Framework web parts built using React illustrating the possibility to use Graph API to invite external users into the Azure Active Directory.
### Invitation manager
Sample SharePoint Framework client-side web part built using React showing how to invite the external user using the Microsoft Graph.
NB. I'm waiting the GA of HttpGraphClient(a bit limited in terms of permission) to use it in this scenario.
Look at this to go deep:
* [HttpGraphClient](https://dev.office.com/sharepoint/docs/spfx/web-parts/guidance/call-microsoft-graph-from-your-web-part)
![The invitation manager web part displayed in SharePoint workbench](./assets/SPFx-Invitation-Manager.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-GA-green.svg)
## Applies to
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution
Solution|Author(s)
--------|---------
react-invitation-manager|Giuliano De Luca ([@giuleon](https://twitter.com/giuleon) , [www.delucagiuliano.com](http://www.delucagiuliano.com))
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|July 14, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Prerequisites
- Office 365 subscription with SharePoint Online and Exchange
## Minimal Path to Awesome
- clone this repo
- in the Azure Active Directory corresponding to your Office 365 tenant register a new Web Application:
- as the **Sign-on URL** enter the URL of the hosted version of SharePoint workbench, eg. *https://contoso.sharepoint.com/_layouts/15/workbench.aspx*
- enable OAuth implicit flow
- grant the application the **Microsoft Graph/Read and write directory data** permission
- copy the application's ID
- in the **src/webparts/invitationManager/AdalConfig.ts** file in the **clientId** property enter the application ID registered in Azure
- in the command line execute
- `npm i`
- `gulp serve --nobrowser`
- navigate to the hosted version of the SharePoint workbench
- add the **Invitation manager** web part
## Features
Sample web part in this solution illustrates the following concepts on top of the SharePoint Framework:
- using React for building SharePoint Framework client-side web parts
- using Office UI Fabric React styles for building user experience consistent with SharePoint and Office
- on-demand authentication with the Azure Active Directory using the ADAL JS library
- communicating with the Microsoft Graph using its REST API
- using the ADAL JS library with SharePoint Framework web parts built using React
![](https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-invitation-manager)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -0,0 +1,13 @@
{
"entries": [
{
"entry": "./lib/webparts/invitationManager/InvitationManagerWebPart.js",
"manifest": "./src/webparts/invitationManager/InvitationManagerWebPart.manifest.json",
"outputPath": "./dist/invitation-manager.bundle.js"
}
],
"externals": {},
"localizedResources": {
"invitationManagerStrings": "webparts/invitationManager/loc/{locale}.js"
}
}

View File

@ -0,0 +1,3 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,6 @@
{
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-aad-implicitflow",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,10 @@
{
"solution": {
"name": "react-invitation-manager-client-side-solution",
"id": "7d08f6e8-b222-4ed7-8910-8ea911d117fe",
"version": "1.0.0.0"
},
"paths": {
"zippedPackage": "solution/react-invitation-manager.sppkg"
}
}

View File

@ -0,0 +1,9 @@
{
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,46 @@
{
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-unused-imports": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false,
"prefer-const": true
}
}
}

View File

@ -0,0 +1,3 @@
{
"cdnBasePath": "https://publiccdn.sharepointonline.com/giuleon.sharepoint.com/cdn/spfx-InvitationManager"
}

View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
{
"name": "react-invitation-manager",
"version": "1.1.0",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"author": {
"name": "Giuliano De Luca",
"url": "http://www.delucagiuliano.com"
},
"dependencies": {
"@microsoft/sp-client-base": "~1.0.0",
"@microsoft/sp-core-library": "~1.0.0",
"@microsoft/sp-webpart-base": "~1.0.0",
"@types/react": "0.14.46",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"adal-angular": "1.0.12",
"react": "15.4.2",
"react-dom": "15.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.0.0",
"@microsoft/sp-module-interfaces": "~1.0.0",
"@microsoft/sp-webpart-workbench": "~1.0.0",
"@types/adal": "^1.0.25",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"gulp": "~3.9.1"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
}
}

View File

@ -0,0 +1,5 @@
export interface IAdalConfig extends adal.Config {
popUp?: boolean;
callback?: (error: any, token: string) => void;
webPartId?: string;
}

View File

@ -0,0 +1,90 @@
const AuthenticationContext = require('adal-angular');
AuthenticationContext.prototype._getItemSuper = AuthenticationContext.prototype._getItem;
AuthenticationContext.prototype._saveItemSuper = AuthenticationContext.prototype._saveItem;
AuthenticationContext.prototype.handleWindowCallbackSuper = AuthenticationContext.prototype.handleWindowCallback;
AuthenticationContext.prototype._renewTokenSuper = AuthenticationContext.prototype._renewToken;
AuthenticationContext.prototype.getRequestInfoSuper = AuthenticationContext.prototype.getRequestInfo;
AuthenticationContext.prototype._addAdalFrameSuper = AuthenticationContext.prototype._addAdalFrame;
AuthenticationContext.prototype._getItem = function (key) {
if (this.config.webPartId) {
key = this.config.webPartId + '_' + key;
}
return this._getItemSuper(key);
};
AuthenticationContext.prototype._saveItem = function (key, object) {
if (this.config.webPartId) {
key = this.config.webPartId + '_' + key;
}
return this._saveItemSuper(key, object);
};
AuthenticationContext.prototype.handleWindowCallback = function (hash) {
if (hash == null) {
hash = window.location.hash;
}
if (!this.isCallback(hash)) {
return;
}
var requestInfo = this.getRequestInfo(hash);
if (requestInfo.requestType === this.REQUEST_TYPE.LOGIN) {
return this.handleWindowCallbackSuper(hash);
}
var resource = this._getResourceFromState(requestInfo.stateResponse);
if (!resource || resource.length === 0) {
return;
}
if (this._getItem(this.CONSTANTS.STORAGE.RENEW_STATUS + resource) === this.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS) {
return this.handleWindowCallbackSuper(hash);
}
}
AuthenticationContext.prototype._renewToken = function (resource, callback) {
this._renewTokenSuper(resource, callback);
var _renewStates = this._getItem('renewStates');
if (_renewStates) {
_renewStates = _renewStates.split(';');
}
else {
_renewStates = [];
}
_renewStates.push(this.config.state);
this._saveItem('renewStates', _renewStates);
}
AuthenticationContext.prototype.getRequestInfo = function (hash) {
var requestInfo = this.getRequestInfoSuper(hash);
var _renewStates = this._getItem('renewStates');
if (!_renewStates) {
return requestInfo;
}
_renewStates = _renewStates.split(';');
for (var i = 0; i < _renewStates.length; i++) {
if (_renewStates[i] === requestInfo.stateResponse) {
requestInfo.requestType = this.REQUEST_TYPE.RENEW_TOKEN;
requestInfo.stateMatch = true;
break;
}
}
return requestInfo;
}
AuthenticationContext.prototype._addAdalFrame = function (iframeId) {
var adalFrame = this._addAdalFrameSuper(iframeId);
adalFrame.style.width = adalFrame.style.height = '106px';
return adalFrame;
}
window.AuthenticationContext = function () {
return undefined;
}

View File

@ -0,0 +1,12 @@
const adalConfig: adal.Config = {
clientId: '2aed3716-947b-4d49-b130-8859eb9363c7',
tenant: 'giuleon.onmicrosoft.com',
extraQueryParameter: 'nux=1',
endpoints: {
'https://graph.microsoft.com': 'https://graph.microsoft.com'
},
postLogoutRedirectUri: window.location.origin,
cacheLocation: 'sessionStorage'
};
export default adalConfig;

View File

@ -0,0 +1,3 @@
export interface IInvitationManagerWebPartProps {
title: string;
}

View File

@ -0,0 +1,20 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "ab186a1f-8553-41ae-879f-3fe50d6488f7",
"alias": "InvitationManagerWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [{
"groupId": "ab186a1f-8553-41ae-879f-3fe50d6488f7",
"group": { "default": "Productivity" },
"title": { "default": "Invitation manager" },
"description": { "default": "Invite the external users" },
"officeFabricIconFontName": "PeopleAdd",
"properties": {
"title": "Invitation manager"
}
}]
}

View File

@ -0,0 +1,55 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'invitationManagerStrings';
import InvitationManager from './components/InvitationManager';
import { IInvitationManagerProps } from './components/IInvitationManagerProps';
import { IInvitationManagerWebPartProps } from './IInvitationManagerWebPartProps';
export default class InvitationManagerWebPart extends BaseClientSideWebPart<IInvitationManagerWebPartProps> {
public render(): void {
const element: React.ReactElement<IInvitationManagerProps> = React.createElement(
InvitationManager,
{
httpClient: this.context.httpClient,
title: this.properties.title,
webPartId: this.context.instanceId
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.ViewGroupName,
groupFields: [
PropertyPaneTextField('title', {
label: strings.TitleFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,15 @@
export interface IExternalUser {
value: [{
id: string,
businessPhones: string,
displayName: string,
givenName: string,
jobTitle: string,
mail: string,
mobilePhone: string,
officeLocation: string,
preferredLanguage: string,
surname: string,
userPrincipalName: string,
}]
}

View File

@ -0,0 +1,9 @@
export interface IInvitation {
id: string;
inviteRedeemUrl: string;
invitedUserDisplayName: string;
invitedUserEmailAddress: string;
sendInvitationMessage: string;
inviteRedirectUrl: string;
status: string;
}

View File

@ -0,0 +1,7 @@
import { HttpClient } from '@microsoft/sp-http';
export interface IInvitationManagerProps {
title: string;
httpClient: HttpClient;
webPartId: string;
}

View File

@ -0,0 +1,16 @@
import { IInvitation } from './IInvitation';
import { IExternalUser } from './IExternalUser';
export interface IInvitationManagerState {
loading: boolean;
error: string;
invitation: IInvitation;
signedIn: boolean;
sendInvitationMessage: boolean;
redirectUrl: string;
displayName: string;
emailAddress: string;
externalUser: IExternalUser,
selectionDetails: string,
items: string[];
}

View File

@ -0,0 +1,103 @@
.invitationManager {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
label {
color: black;
}
.button {
&.buttonCompound {
text-align: left;
display: block;
max-width: 280px;
min-height: 72px;
padding: 20px;
background: #f4f4f4;
border: 1px solid #f4f4f4;
.buttonLabel {
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
font-weight: 400;
display: block;
font-weight: 600;
color: #000000;
margin-top: -5px;
}
.buttonDescription {
text-align: left;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 12px;
font-weight: 400;
color: #666666;
display: block;
position: relative;
top: 3px;
}
}
}
.spinner {
display: inline-block;
margin: 10px 0;
@-webkit-keyframes spinnerSpin {
0% {
-webkit-transform:rotate(0);
transform:rotate(0);
}
100% {
-webkit-transform:rotate(360deg);
transform:rotate(360deg);
}
}
@keyframes spinnerSpin {
0% {
-webkit-transform:rotate(0);
transform:rotate(0);
}
100% {
-webkit-transform:rotate(360deg);
transform:rotate(360deg);
}
}
.spinnerCircle {
margin: auto;
box-sizing: border-box;
border-radius: 50%;
width: 100%;
height: 100%;
border: 1.5px solid #c7e0f4;
border-top-color: #0078d7;
-webkit-animation: spinnerSpin 1.3s infinite cubic-bezier(.53, .21, .29, .67);
animation: spinnerSpin 1.3s infinite cubic-bezier(.53, .21, .29, .67);
&.spinnerNormal {
width: 20px;
height: 20px;
}
}
.spinnerLabel {
color: #0078d7;
margin-top: 10px;
text-align: center;
}
}
}

View File

@ -0,0 +1,418 @@
import * as React from 'react';
import styles from './InvitationManager.module.scss';
import { IInvitationManagerProps } from './IInvitationManagerProps';
import { IInvitationManagerState } from './IInvitationManagerState';
import { IInvitation } from './IInvitation';
import { IExternalUser } from './IExternalUser';
import { escape } from '@microsoft/sp-lodash-subset';
import { HttpClient, IHttpClientOptions, HttpClientResponse } from '@microsoft/sp-http';
import * as AuthenticationContext from 'adal-angular';
import adalConfig from '../AdalConfig';
import { IAdalConfig } from '../../IAdalConfig';
import '../../WebPartAuthenticationContext';
import { PrimaryButton, IButtonProps, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
import { autobind } from 'office-ui-fabric-react/lib/Utilities';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import {
DetailsList,
DetailsListLayoutMode,
Selection
} from 'office-ui-fabric-react/lib/DetailsList';
import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection';
let _items = [];
let _columns = [
{
key: 'column1',
name: 'Name',
fieldName: 'name',
minWidth: 100,
maxWidth: 200,
isResizable: true
},
{
key: 'column2',
name: 'Value',
fieldName: 'value',
minWidth: 100,
maxWidth: 200,
isResizable: true
},
];
export default class InvitationManager extends React.Component<IInvitationManagerProps, IInvitationManagerState> {
private _selection: Selection;
private authCtx: adal.AuthenticationContext;
constructor(props: IInvitationManagerProps, context?: any) {
super(props);
this._selection = new Selection({
onSelectionChanged: () =>
//this.setState({ selectionDetails: this._getSelectionDetails() })
this.setState((previousState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
previousState.selectionDetails = this._getSelectionDetails();
return previousState;
})
});
this.state = {
loading: false,
error: null,
invitation: null,
signedIn: false,
sendInvitationMessage: true,
redirectUrl: '',
emailAddress: '',
displayName: '',
externalUser: null,
selectionDetails: this._getSelectionDetails(),
items: [],
};
const config: IAdalConfig = adalConfig;
config.popUp = true;
config.webPartId = this.props.webPartId;
config.callback = (error: any, token: string): void => {
this.setState((previousState: IInvitationManagerState, currentProps: IInvitationManagerProps): IInvitationManagerState => {
previousState.error = error;
previousState.signedIn = !(!this.authCtx.getCachedUser());
return previousState;
});
};
this.authCtx = new AuthenticationContext(config);
AuthenticationContext.prototype._singletonInstance = undefined;
}
public componentDidMount(): void {
this.authCtx.handleWindowCallback();
if (window !== window.top) {
return;
}
this.setState((previousState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
previousState.error = this.authCtx.getLoginError();
previousState.signedIn = !(!this.authCtx.getCachedUser());
return previousState;
});
}
public componentDidUpdate(prevProps: IInvitationManagerProps, prevState: IInvitationManagerState, prevContext: any): void {
if (prevState.signedIn !== this.state.signedIn) {
//this._sendInvitation();
this._getExternalUser();
}
}
public render(): React.ReactElement<IInvitationManagerProps> {
const login: JSX.Element = this.state.signedIn ? <div /> : <button className={`${styles.button} ${styles.buttonCompound}`} onClick={() => { this.signIn(); } }><span className={styles.buttonLabel}>Sign in</span></button>;
const loading: JSX.Element = this.state.loading ? <div style={{ margin: '0 auto', width: '7em' }}><div className={styles.spinner}><div className={`${styles.spinnerCircle} ${styles.spinnerNormal}`}></div><div className={styles.spinnerLabel}>Loading...</div></div></div> : <div/>;
const error: JSX.Element = this.state.error ? <div><strong>Error: </strong> {this.state.error}</div> : <div/>;
const invitedUserDisplayName: JSX.Element = this.state.invitation ? <div><strong>Display Name: </strong> {this.state.invitation.invitedUserDisplayName}</div> : <div/>;
const invitedUserEmailAddress: JSX.Element = this.state.invitation ? <div><strong>Email Address: </strong> {this.state.invitation.invitedUserEmailAddress}</div> : <div/>;
const status: JSX.Element = this.state.invitation ? <div><strong>Status: </strong> {this.state.invitation.status}</div> : <div/>;
let { externalUser, selectionDetails } = this.state;
return (
<div className={styles.invitationManager}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-u-lg9 ms-u-xl9">
<span className="ms-font-xl">{escape(this.props.title)}</span>
<p className="ms-font-l">Invite external user</p>
<TextField onChanged={ this._displayName_onChanged } label='Display' placeholder='Insert the display name of the external user' ariaLabel='Please enter text here' />
<TextField onChanged={ this._eMail_onChanged } label='Email' iconClass='ms-Icon--Mail ms-Icon' placeholder='Insert the email address of the external user' ariaLabel='Please enter text here' />
<TextField onChanged={ this._redirectURL_onChanged } label='Redirect URL' placeholder='Where do you want redirect the external user' ariaLabel='Please enter text here' />
<Toggle
defaultChecked={ this.state.sendInvitationMessage }
label='Send invitation message'
onText='On'
offText='Off'
onChanged={ this._sendInvitationMessage_onChanged } />
<div>
{loading}
{error}
{invitedUserDisplayName}
{invitedUserEmailAddress}
{status}
</div>
<DefaultButton
data-automation-id='test'
description='I am a description'
onClick={() => { this._sendInvitation(); } } >
<span className={styles.buttonDescription}>Invite the user</span>
</DefaultButton>
</div>
<div className="ms-Grid-col ms-u-lg3 ms-u-xl3">
{login}
</div>
</div>
{/*Grid of external users*/}
<div className={`ms-Grid-row ms-bgColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-u-lg10 ms-u-xl12">
<p className="ms-font-l">External users in your organization</p>
<div>
<div>{ selectionDetails }</div>
<TextField
label='Filter by name'
onChanged={ text =>
//this.setState({ externalUser: text ? _items.filter(i => i.name.toLowerCase().indexOf(text) > -1) : _items }) }
this.setState((previousState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
previousState.items = text ? _items.filter(i => i.name.toLowerCase().indexOf(text) > -1) : _items;
return previousState;
})
}
/>
<MarqueeSelection selection={ this._selection }>
<DetailsList
items={ this.state.items }
columns={ _columns }
setKey='set'
layoutMode={ DetailsListLayoutMode.fixedColumns }
selection={ this._selection }
selectionPreservedOnEmptyClick={ true }
onItemInvoked={ (item) => alert(`Item invoked: ${item.name}`) }
/>
</MarqueeSelection>
</div>
</div>
</div>
</div>
</div>
);
}
private _getSelectionDetails(): string {
let selectionCount = this._selection.getSelectedCount();
switch (selectionCount) {
case 0:
return 'No items selected';
case 1:
return '1 item selected: ' + (this._selection.getSelection()[0] as any).name;
default:
return `${selectionCount} items selected`;
}
}
@autobind
private _sendInvitationMessage_onChanged(cheked: boolean): void {
this.setState((previousState: IInvitationManagerState, currentProps: IInvitationManagerProps): IInvitationManagerState => {
previousState.sendInvitationMessage = cheked;
return previousState;
});
}
@autobind
private _redirectURL_onChanged(newValue: any): void {
this.setState((previousState: IInvitationManagerState, currentProps: IInvitationManagerProps): IInvitationManagerState => {
previousState.redirectUrl = newValue;
return previousState;
});
}
@autobind
private _eMail_onChanged(newValue: any): void {
this.setState((previousState: IInvitationManagerState, currentProps: IInvitationManagerProps): IInvitationManagerState => {
previousState.emailAddress = newValue;
return previousState;
});
}
@autobind
private _displayName_onChanged(newValue: any): void {
this.setState((previousState: IInvitationManagerState, currentProps: IInvitationManagerProps): IInvitationManagerState => {
previousState.displayName = newValue;
return previousState;
});
}
private _sendInvitation = (ev?:React.MouseEvent<HTMLButtonElement>) => {
// Prevent postback
ev ? ev.preventDefault() : null;
if (this.state.signedIn !== false) {
this.sendInvitation();
}
}
private _getExternalUser = () => {
if (this.state.signedIn !== false) {
this.getExternalUser();
}
}
public signIn(): void {
this.authCtx.login();
}
private getExternalUser(): void {
this.setState((previousState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
previousState.loading = true;
return previousState;
});
this.getGraphAccessToken()
.then((accessToken: string): Promise<IExternalUser> => {
console.log(accessToken);
return InvitationManager.getExternalUser(accessToken, this.props.httpClient, this.state);
})
.then((external: IExternalUser): void => {
this.setState((prevState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
prevState.loading = false;
prevState.externalUser = external;
// Populate with external users items.
if (external !== null) {
for (let i = 0; i < external.value.length; i++) {
_items.push({
key: i,
name: external.value[i].displayName,
value: external.value[i].mail,
});
}
}
prevState.items = _items;
return prevState;
});
}, (error: any): void => {
this.setState((prevState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
prevState.loading = false;
prevState.error = error;
return prevState;
});
});
}
private sendInvitation(): void {
this.setState((previousState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
previousState.loading = true;
return previousState;
});
this.getGraphAccessToken()
.then((accessToken: string): Promise<IInvitation> => {
console.log(accessToken);
return InvitationManager.postInvitation(accessToken, this.props.httpClient, this.state);
})
.then((invitation: IInvitation): void => {
this.setState((prevState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
prevState.loading = false;
prevState.invitation = invitation;
return prevState;
});
}, (error: any): void => {
this.setState((prevState: IInvitationManagerState, props: IInvitationManagerProps): IInvitationManagerState => {
prevState.loading = false;
prevState.error = error;
return prevState;
});
});
}
private getGraphAccessToken(): Promise<string> {
return new Promise<string>((resolve: (accessToken: string) => void, reject: (error: any) => void): void => {
const graphResource: string = 'https://graph.microsoft.com';
const accessToken: string = this.authCtx.getCachedToken(graphResource);
if (accessToken) {
console.log('ACCESS TOKEN: ' + accessToken);
resolve(accessToken);
return;
}
if (this.authCtx.loginInProgress()) {
reject('Login already in progress');
return;
}
this.authCtx.acquireToken(graphResource, (error: string, token: string) => {
if (error) {
reject(error);
return;
}
if (token) {
resolve(token);
}
else {
reject('Couldn\'t retrieve access token');
}
});
});
}
private static getExternalUser(accessToken: string, httpClient: HttpClient, previousState: IInvitationManagerState): Promise<IExternalUser> {
const URL = `https://graph.microsoft.com/v1.0/users?$filter=userType eq 'Guest'`;
const requestHeaders: Headers = new Headers();
requestHeaders.append('Accept', 'application/json');
//For an OAuth token
requestHeaders.append('Authorization', 'Bearer ' + accessToken);
const httpClientOptions: IHttpClientOptions = { headers: requestHeaders };
return new Promise<IExternalUser>((resolve: (external: IExternalUser) => void, reject: (error: any) => void): void => {
httpClient.get(URL, HttpClient.configurations.v1, httpClientOptions)
.then((response: HttpClientResponse): Promise<IExternalUser> => {
return response.json();
})
.then((externalResponse: IExternalUser): void => {
resolve(externalResponse);
}, (error: any): void => {
reject(error);
});
});
}
private static postInvitation(accessToken: string, httpClient: HttpClient, previousState: IInvitationManagerState): Promise<IInvitation> {
const postURL = `https://graph.microsoft.com/v1.0/invitations`;
const body: string = JSON.stringify({
'invitedUserDisplayName': previousState.displayName,
'invitedUserEmailAddress': previousState.emailAddress,
'inviteRedirectUrl': previousState.redirectUrl,
"sendInvitationMessage": previousState.sendInvitationMessage,
});
const requestHeaders: Headers = new Headers();
requestHeaders.append('Content-type', 'application/json');
requestHeaders.append('Cache-Control', 'no-cache');
//For an OAuth token
requestHeaders.append('Authorization', 'Bearer ' + accessToken);
//For Basic authentication requestHeaders.append('Authorization', 'Basic <CREDENTIALS>');
const httpClientOptions: IHttpClientOptions = { body: body, headers: requestHeaders };
return new Promise<IInvitation>((resolve: (invitation: IInvitation) => void, reject: (error: any) => void): void => {
httpClient.post(postURL, HttpClient.configurations.v1, httpClientOptions)
.then((response: HttpClientResponse): Promise<IInvitation> => {
return response.json();
})
.then((invitationResponse: IInvitation ): void => {
const invitation: IInvitation = {
id: '',
inviteRedeemUrl: '',
invitedUserDisplayName: '',
invitedUserEmailAddress: '',
sendInvitationMessage: '',
inviteRedirectUrl: '',
status: ''
};
invitation.id = invitationResponse.id;
invitation.invitedUserDisplayName = invitationResponse.invitedUserDisplayName;
invitation.invitedUserEmailAddress = invitationResponse.invitedUserEmailAddress;
invitation.inviteRedeemUrl = invitationResponse.inviteRedeemUrl;
invitation.inviteRedirectUrl = invitationResponse.inviteRedirectUrl;
invitation.sendInvitationMessage = invitationResponse.sendInvitationMessage;
invitation.status = invitationResponse.status;
resolve(invitation);
}, (error: any): void => {
reject(error);
});
});
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Manage the settings of this Web Part",
"ViewGroupName": "View",
"TitleFieldLabel": "Web Part Title (displayed in the body)"
}
});

View File

@ -0,0 +1,10 @@
declare interface IInvitationManagerStrings {
PropertyPaneDescription: string;
ViewGroupName: string;
TitleFieldLabel: string;
}
declare module 'invitationManagerStrings' {
const strings: IInvitationManagerStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('InvitationManagerWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"adal",
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,8 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />