Sample Spfx Webpart read / update MS Graph Custom Schema Extensions (#552)

This commit is contained in:
Luis Manez 2018-07-03 20:22:49 +02:00 committed by Vesa Juvonen
parent 5fe4521c21
commit 6a459d0942
23 changed files with 18618 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,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,12 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": true,
"isCreatingSolution": true,
"environment": "spo",
"version": "1.5.1",
"libraryName": "react-graph-schema-extensions",
"libraryId": "9efd07c8-6e84-40e5-9c8f-a1800322080e",
"packageManager": "npm",
"componentType": "webpart"
}
}

View File

@ -0,0 +1,95 @@
# Spfx Webpart read / update MS Graph Custom Schema Extensions
## Summary
This sample shows how read and update a custom Schema extension in MS Graph. It shows how to create a
custom Schema extension in Graph to store custom data related to an Office 365 Group, and how we can read and update
that data using an spfx webpart.
A possible business scenario here could be if we want to store some additional custom data related to some specific
Office 365 Groups, for instance Sales information, and make it available in the SharePoint site.
![Custom Schema Extension Webpart](./assets/webpart.png)
## Used SharePoint Framework Version
![SPFx v1.5.1](https://img.shields.io/badge/SPFx-1.5.1-green.svg)
## Applies to
* [SharePoint Framework Developer](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-graph-schema-extensions|Luis Mañez (MVP, [ClearPeople](http://www.clearpeople.com), @luismanez)
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|Jul 03, 2018|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
* Create a custom extenion for Groups using Graph API: Currently, spfx has no permissions to create custom extensions
for entities in Graph API. To create the custom extension, you can use the [MS Graph Explorer website](https://developer.microsoft.com/en-us/graph/graph-explorer).
To create the extension you must do a POST request to:
```js
POST https://graph.microsoft.com/v1.0/schemaExtensions
content-type: application/json
{
"id": "inheritscloud_SalesCustomData",
"description": "Adding custom data to Groups created for sales",
"owner": "ac638f16-63c2-462b-95a4-16f8a60b0628",
"targetTypes": [
"Group"
],
"properties": [
{
"name": "businessUnit", "type": "String"
},
{
"name": "estimatedBudget", "type": "Integer"
},
{
"name": "expectedClosedDate", "type": "DateTime"
}
]
}
```
See here for more information about the attributes: [https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/schemaextension](https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/schemaextension)
__Note__:
* For the _id_ attribute, You can assign a value in one of two ways:
* Concatenate the name of one of your verified domains with a name for the schema extension to form a unique string in this format, {domainName}_{schemaName}. As an example, contoso_mySchema.
* Provide a schema name, and let Microsoft Graph use that schema name to complete the id assignment in this format: ext{8-random-alphanumeric-chars}_{schema-name}. An example would be extkvbmkofy_mySchema.
* The _owner_ attribute must be a valid ClientId registered in Azure AD
* The _targetTypes_ is an array with the different Entities that you want to extend (users, groups, event, message). However, spfx only allows to update Groups, so the value is set to _group_
## Minimal Path to Awesome
* clone repo
* edit _GraphSchemaExtenion.tsx_ file and change line 141 with the _id_ "inheritscloud_SalesCustomData" assigned when you created the custom Schema extension
* run _gulp serve_
## Features
This sample shows how read and update a custom Schema extension in MS Graph.
This sample illustrates the following concepts on top of the SharePoint Framework:
* How to create a custom schema extension in Graph API using Graph Explorer tool
* Using GraphHttpClient to get data from MS Graph API
* How to update an MS Graph entity (in this case, Office 365 Group) with custom data
* Using async / await for the async calls
* Office UI fabric components

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"graph-schema-extension-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/graphSchemaExtension/GraphSchemaExtensionWebPart.js",
"manifest": "./src/webparts/graphSchemaExtension/GraphSchemaExtensionWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"GraphSchemaExtensionWebPartStrings": "lib/webparts/graphSchemaExtension/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-graph-schema-extensions",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,12 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-graph-schema-extensions-client-side-solution",
"id": "9efd07c8-6e84-40e5-9c8f-a1800322080e",
"version": "1.0.0.0",
"includeClientSideAssets": true
},
"paths": {
"zippedPackage": "solution/react-graph-schema-extensions.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/tslint.schema.json",
// 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-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
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
{
"name": "react-graph-schema-extensions",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "15.6.2",
"react-dom": "15.6.2",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@microsoft/sp-core-library": "1.5.1-plusbeta",
"@microsoft/sp-webpart-base": "1.5.1-plusbeta",
"@microsoft/sp-lodash-subset": "1.5.1-plusbeta",
"@microsoft/sp-office-ui-fabric-core": "1.5.1-plusbeta",
"@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.5.1-plusbeta",
"@microsoft/sp-module-interfaces": "1.5.1-plusbeta",
"@microsoft/sp-webpart-workbench": "1.5.1-plusbeta",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "b481f418-6058-4514-8b68-d500bd3bab42",
"alias": "GraphSchemaExtensionWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "GraphSchemaExtension" },
"description": { "default": "Sample on how to get custom schema extensions using MS Graph API and how to update the value" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "GraphSchemaExtension"
}
}]
}

View File

@ -0,0 +1,60 @@
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 "GraphSchemaExtensionWebPartStrings";
import GraphSchemaExtension from "./components/GraphSchemaExtension";
import { IGraphSchemaExtensionProps } from "./components/IGraphSchemaExtensionProps";
export interface IGraphSchemaExtensionWebPartProps {
description: string;
}
export default class GraphSchemaExtensionWebPart extends BaseClientSideWebPart<IGraphSchemaExtensionWebPartProps> {
public render(): void {
const element: React.ReactElement<IGraphSchemaExtensionProps > = React.createElement(
GraphSchemaExtension,
{
context: this.context
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("description", {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.graphSchemaExtension {
.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 {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,225 @@
import * as React from "react";
import { escape } from "@microsoft/sp-lodash-subset";
import styles from "./GraphSchemaExtension.module.scss";
import { IGraphSchemaExtensionProps, IGraphSchemaExtensionState, ISalesDataSchemaExtension } from "./IGraphSchemaExtensionProps";
import { GraphHttpClient, HttpClientResponse, GraphHttpClientConfiguration, IGraphHttpClientOptions } from "@microsoft/sp-http";
import { Guid } from "@microsoft/sp-core-library";
import { TextField, DefaultButton } from "office-ui-fabric-react";
export default class GraphSchemaExtension extends React.Component<IGraphSchemaExtensionProps, IGraphSchemaExtensionState> {
private graphClient: GraphHttpClient;
private _onBusinessUnitChanged(value: any): void {
let stateCopy: ISalesDataSchemaExtension = this.state.data;
stateCopy.businessUnit = value;
this.setState({
data: stateCopy
});
}
private _onEstimatedBudgetChanged(value: any): void {
let stateCopy: ISalesDataSchemaExtension = this.state.data;
stateCopy.estimatedBudget = value;
this.setState({
data: stateCopy
});
}
private _onExpectedClosedDateChanged(value: any): void {
let stateCopy: ISalesDataSchemaExtension = this.state.data;
stateCopy.expectedClosedDate = value;
this.setState({
data: stateCopy
});
}
constructor() {
super();
this._update = this._update.bind(this);
this._onBusinessUnitChanged = this._onBusinessUnitChanged.bind(this);
this._onEstimatedBudgetChanged = this._onEstimatedBudgetChanged.bind(this);
this._onExpectedClosedDateChanged = this._onExpectedClosedDateChanged.bind(this);
this.state = {
data: null
};
}
/// so far is not allowed to create an Extension using GraphClient
/// but if MS grant more permissions in future, this code should work
/// current scopes allowed: Group.ReadWrite.All Reports.Read.All User.Read.All
private async _createSchemaExtension(): Promise<any> {
const httpClientOptions: IGraphHttpClientOptions = {
body: JSON.stringify({
"id": "inheritscloud_SalesCustomData02",
"description": "Adding custom data to Groups created for sales",
"owner": "ac638f16-63c2-462b-95a4-16f8a60b0628",
"targetTypes": [
"Group"
],
"properties": [
{
"name": "businessUnit", "type": "String"
},
{
"name": "estimatedBudget", "type": "Integer"
},
{
"name": "expectedClosedDate", "type": "DateTime"
}
]
})
};
const response: HttpClientResponse = await this.graphClient.post(
"v1.0/schemaExtensions",
GraphHttpClient.configurations.v1, httpClientOptions);
const responseJson: any = await response.json();
return responseJson;
}
/// not allowed so far. Even for your own user, you can´t update a custom Schema ext.
private async _updateMeUser(): Promise<any> {
const httpClientOptions: IGraphHttpClientOptions = {
method: "PATCH",
body: JSON.stringify({
"inheritscloud_SocialData": {
"twitter": "@luismanez",
"facebook": "facebook.com/luismanez",
"lastEvent": "2018-06-09T10:30:00",
"isSpeaker": true
}
})
};
const response: HttpClientResponse = await this.graphClient.fetch(
"v1.0/me",
GraphHttpClient.configurations.v1,
httpClientOptions);
const responseJson: any = await response.json();
return responseJson;
}
private async _getCustomExtension(): Promise<ISalesDataSchemaExtension> {
const groupId: Guid = this.props.context.pageContext.site.group.id;
const response: HttpClientResponse = await this.graphClient.get(
`v1.0/groups/${groupId}/?$select=id,displayName,inheritscloud_SalesCustomData`,
GraphHttpClient.configurations.v1);
const responseJson: any = await response.json();
const groupSchemaExtenion: ISalesDataSchemaExtension = {
id: responseJson.id,
displayName: responseJson.displayName,
expectedClosedDate: responseJson.inheritscloud_SalesCustomData &&
responseJson.inheritscloud_SalesCustomData.expectedClosedDate,
estimatedBudget: responseJson.inheritscloud_SalesCustomData &&
responseJson.inheritscloud_SalesCustomData.estimatedBudget,
businessUnit: responseJson.inheritscloud_SalesCustomData &&
responseJson.inheritscloud_SalesCustomData.businessUnit
};
return groupSchemaExtenion;
}
private async _updateExtensionInGroup(): Promise<any> {
console.log("About to update Extension with data: ", this.state.data);
const httpClientOptions: IGraphHttpClientOptions = {
method: "PATCH",
body: JSON.stringify({
"inheritscloud_SalesCustomData": {
"businessUnit": this.state.data.businessUnit,
"estimatedBudget": this.state.data.estimatedBudget,
"expectedClosedDate": this.state.data.expectedClosedDate
}
})
};
const groupId: Guid = this.props.context.pageContext.site.group.id;
const response: HttpClientResponse = await this.graphClient.fetch(
`v1.0/groups/${groupId}`,
GraphHttpClient.configurations.v1,
httpClientOptions);
return response.status;
}
private async _update(): Promise<void> {
console.log(this.state.data);
const updated: any = await this._updateExtensionInGroup();
if (updated === 204) {
console.log("Data updated successfuly");
} else {
console.log("Error updating data");
}
}
public componentWillMount(): void {
this.graphClient = this.props.context.graphHttpClient;
}
public componentDidMount(): void {
this._getCustomExtension().then((value) => {
console.log(value);
this.setState({
data: value
});
}).catch((error: any) => {
console.log(error);
this.setState({
data: {id: null, displayName: "ERROR"}
});
});
}
public render(): React.ReactElement<IGraphSchemaExtensionProps> {
let content: any = <div>Loading ...</div>;
if (this.state.data !== null) {
content = <div>
<h3>{this.state.data.displayName} ({this.state.data.id})</h3>
<div>
<TextField label="Business Unit"
defaultValue={this.state.data.businessUnit}
onChanged={this._onBusinessUnitChanged} />
</div>
<div>
<TextField label="Estimated Budget"
defaultValue={this.state.data.estimatedBudget.toString()}
onChanged={this._onEstimatedBudgetChanged} />
</div>
<div>
<TextField label="Expected Closed Date"
defaultValue={this.state.data.expectedClosedDate.toString()}
onChanged={this._onExpectedClosedDateChanged} />
</div>
<DefaultButton
primary={true}
text="Update"
onClick={this._update}
/>
</div>;
}
return (
<div className={ styles.container }>
<h2>Group sales data (MS Graph custom Schema extension)</h2>
{ content }
</div>
);
}
}

View File

@ -0,0 +1,17 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface ISalesDataSchemaExtension {
id: string;
displayName: string;
expectedClosedDate?: Date;
estimatedBudget?: number;
businessUnit?: string;
}
export interface IGraphSchemaExtensionProps {
context: WebPartContext;
}
export interface IGraphSchemaExtensionState {
data: ISalesDataSchemaExtension;
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IGraphSchemaExtensionWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'GraphSchemaExtensionWebPartStrings' {
const strings: IGraphSchemaExtensionWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}