spfx webpart to Tag Teams using a TermSet in SP (#845)

This commit is contained in:
Luis Manez 2019-04-18 20:08:35 +02:00 committed by Vesa Juvonen
parent 741abf4b70
commit 8bddd30075
29 changed files with 18187 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

32
samples/react-teams-tagging/.gitignore vendored Normal file
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": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.8.0",
"libraryName": "react-teams-tagging",
"libraryId": "477b4122-5134-404a-8023-fbbb96b135ce",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,92 @@
# spfx webpart to Tag Teams using a TermSet in SharePoint
## Summary
This sample shows how read and update a custom Schema extension in MS Graph to Tag a Team using metadata from a specific TermSet in SharePoint. The user can select different Terms (up to 3), and are saved as a custom Schema extension in Group Graph entity. Besides, the webpart allows to find other Teams tagged wit the same Tag (for demo puposes only 1st Tag is used to run the query)
![Custom Schema Extension Webpart](./assets/webpart.jpg)
## Used SharePoint Framework Version
![SPFx v1.8.0](https://img.shields.io/badge/SPFx-1.8.0-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-teams-tagging|Luis Mañez (MVP, [ClearPeople](http://www.clearpeople.com), @luismanez)
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|Apr 18, 2019|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_TeamsTagging",
"description": "Adding Tags to Teams",
"owner": "bbb1b0ef-2f6b-4b50-bcc9-b6a062f202c2",
"targetTypes": [
"Group"
],
"properties": [
{
"name": "tag1", "type": "String"
},
{
"name": "tag2", "type": "String"
},
{
"name": "tag3", "type": "String"
}
]
}
```
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 _TeamsTagging.tsx_ file and change lines 30, 47, 86 with the _id_ "inheritscloud_TeamsTagging" 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. The schema extension is composed of 3 string values (list of Tags). For demo purposes we are not storing the ID of the Terms, only the label (and the TaxonomyPicker is not bound with the stored values)
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
* Using TaxonomyPicker control from spfx PnP React controls
* How to update an MS Graph entity (in this case, Office 365 Group) with custom data
* How to filter Graph data using a custom Schema Extension
* Using async / await for the async calls
* Office UI fabric components

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"teams-tagging-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/teamsTagging/TeamsTaggingWebPart.js",
"manifest": "./src/webparts/teamsTagging/TeamsTaggingWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"TeamsTaggingWebPartStrings": "lib/webparts/teamsTagging/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/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-teams-tagging",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-teams-tagging-client-side-solution",
"id": "477b4122-5134-404a-8023-fbbb96b135ce",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-teams-tagging.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,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);

17428
samples/react-teams-tagging/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "react-teams-tagging",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.8.0",
"@microsoft/sp-lodash-subset": "1.8.0",
"@microsoft/sp-office-ui-fabric-core": "1.8.0",
"@microsoft/sp-property-pane": "1.8.0",
"@microsoft/sp-webpart-base": "1.8.0",
"@pnp/spfx-controls-react": "1.12.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.4.2",
"@types/react-dom": "16.0.5",
"@types/webpack-env": "1.13.1",
"react": "16.7.0",
"react-dom": "16.7.0"
},
"resolutions": {
"@types/react": "16.4.2"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.3": "0.1.6",
"@microsoft/sp-build-web": "1.8.0",
"@microsoft/sp-module-interfaces": "1.8.0",
"@microsoft/sp-tslint-rules": "1.8.0",
"@microsoft/sp-webpart-workbench": "1.8.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"tslint": "^5.16.0",
"typescript": "3.3.4000"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,23 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "0421c2b0-90f0-429f-83e6-f3987e417cae",
"alias": "TeamsTaggingWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart", "TeamsTab"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "TeamsTagging" },
"description": { "default": "Webpart to Tag Teams based on a SharePoint TermSet" },
"officeFabricIconFontName": "Page",
"properties": {
"termSetId": "4a076cae-831c-4882-9b54-0f54f888e1fc"
}
}]
}

View File

@ -0,0 +1,61 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import * as strings from 'TeamsTaggingWebPartStrings';
import TeamsTagging from './components/TeamsTagging';
import { ITeamsTaggingProps } from './components/ITeamsTaggingProps';
export interface ITeamsTaggingWebPartProps {
termSetId: string;
}
export default class TeamsTaggingWebPart extends BaseClientSideWebPart<ITeamsTaggingWebPartProps> {
public render(): void {
const element: React.ReactElement<ITeamsTaggingProps > = React.createElement(
TeamsTagging,
{
termSetId: this.properties.termSetId,
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('termSetId', {
label: strings.TermSetIdFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,12 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface ITeamsTaggingProps {
termSetId: string;
context: WebPartContext;
}
export interface ITeamInfo {
id: string;
name: string;
tags: string[];
}

View File

@ -0,0 +1,7 @@
import { ITeamInfo } from "./ITeamsTaggingProps";
export interface ITeamsTaggingState {
selectedTags: string[];
savedTags: string[];
similarTeams: ITeamInfo[];
}

View File

@ -0,0 +1,75 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.teamsTagging {
.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;
color: white;
}
}
}

View File

@ -0,0 +1,179 @@
import * as React from 'react';
import styles from './TeamsTagging.module.scss';
import { ITeamsTaggingProps, ITeamInfo } from './ITeamsTaggingProps';
import { TaxonomyPicker, IPickerTerms } from "@pnp/spfx-controls-react/lib/TaxonomyPicker";
import { ITeamsTaggingState } from './ITeamsTaggingState';
import { DefaultButton } from "office-ui-fabric-react";
import { Guid } from '@microsoft/sp-core-library';
import { HttpClientResponse, GraphHttpClient, IGraphHttpClientOptions } from '@microsoft/sp-http';
export default class TeamsTagging extends React.Component<ITeamsTaggingProps, ITeamsTaggingState> {
constructor(props: ITeamsTaggingProps) {
super(props);
this._findSimilarTeams = this._findSimilarTeams.bind(this);
this._updateTeamTags = this._updateTeamTags.bind(this);
this._onTaxPickerChange = this._onTaxPickerChange.bind(this);
this.state = {
selectedTags: [],
savedTags: [],
similarTeams: []
};
}
private async _getTeamTags(): Promise<string[]> {
const groupId: Guid = this.props.context.pageContext.site.group.id;
const response: HttpClientResponse = await this.props.context.graphHttpClient.get(
`v1.0/groups/${groupId}/?$select=id,displayName,inheritscloud_TeamsTagging`,
GraphHttpClient.configurations.v1);
const responseJson: any = await response.json();
let tags: string[] = [];
if (responseJson.inheritscloud_TeamsTagging.tag1) tags.push(responseJson.inheritscloud_TeamsTagging.tag1);
if (responseJson.inheritscloud_TeamsTagging.tag2) tags.push(responseJson.inheritscloud_TeamsTagging.tag2);
if (responseJson.inheritscloud_TeamsTagging.tag3) tags.push(responseJson.inheritscloud_TeamsTagging.tag3);
return tags;
}
private async _findSimilarTeams(): Promise<void> {
const tag1 = this.state.savedTags[0];
const response: HttpClientResponse = await this.props.context.graphHttpClient.get(
`v1.0/groups/?$filter=inheritscloud_TeamsTagging/tag1 eq '${tag1}'&$select=id,displayName,inheritscloud_TeamsTagging`,
GraphHttpClient.configurations.v1);
const responseJson: any = await response.json();
const similarTeams = responseJson.value.map((team) => {
let tags: string[] = [];
if (team.inheritscloud_TeamsTagging.tag1) tags.push(team.inheritscloud_TeamsTagging.tag1);
if (team.inheritscloud_TeamsTagging.tag2) tags.push(team.inheritscloud_TeamsTagging.tag2);
if (team.inheritscloud_TeamsTagging.tag3) tags.push(team.inheritscloud_TeamsTagging.tag3);
const similarTeam: ITeamInfo = {
id: team.id,
name: team.displayName,
tags: tags
};
return similarTeam;
});
this.setState({
similarTeams: similarTeams
});
}
private async _updateTeamTags(): Promise<void> {
const updated: any = await this._updateExtensionInGroup();
if (updated === 204) {
this.setState({
savedTags: this.state.selectedTags
});
} else {
console.log("Error updating data");
}
}
private async _updateExtensionInGroup(): Promise<any> {
const httpClientOptions: IGraphHttpClientOptions = {
method: "PATCH",
body: JSON.stringify({
"inheritscloud_TeamsTagging": {
"tag1": this.state.selectedTags[0] ? this.state.selectedTags[0] : '',
"tag2": this.state.selectedTags[1] ? this.state.selectedTags[1] : '',
"tag3": this.state.selectedTags[2] ? this.state.selectedTags[2] : ''
}
})
};
const groupId: Guid = this.props.context.pageContext.site.group.id;
const response: HttpClientResponse = await this.props.context.graphHttpClient.fetch(
`v1.0/groups/${groupId}`,
GraphHttpClient.configurations.v1,
httpClientOptions);
return response.status;
}
public componentDidMount(): void {
this._getTeamTags().then((value) => {
console.log(value);
this.setState({
savedTags: value
});
});
}
public render(): React.ReactElement<ITeamsTaggingProps> {
let tags: any = <div>Getting Team tags from Graph API ...</div>;
if (this.state.savedTags.length > 0) {
tags = <div><h2>Team tags:</h2><ul>
{this.state.savedTags.map(t => <li>{t}</li>)}
</ul></div>;
}
let similarTeams: any;
if (this.state.similarTeams.length > 0) {
similarTeams = <div><h3>Similar teams:</h3><ul>
{this.state.similarTeams.map(t => <li>{t.name}</li>)}
</ul></div>;
}
return (
<div className={styles.teamsTagging}>
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<div>{tags}</div>
<div className={styles.description}>
<TaxonomyPicker allowMultipleSelections={true}
termsetNameOrID={this.props.termSetId}
panelTitle="Select Term"
label="Select Tags for Team/Group..."
context={this.props.context}
onChange={this._onTaxPickerChange}
isTermSetSelectable={false} />
<DefaultButton
primary={true}
text="Update Team Tags"
onClick={this._updateTeamTags}
/>
</div>
</div>
</div>
<div className={styles.row}>
<div className={styles.column}>
<DefaultButton
primary={true}
text="Find similar Teams *"
onClick={this._findSimilarTeams}
/>
* (only using 1st tag)
{similarTeams}
</div>
</div>
</div>
</div>
);
}
private _onTaxPickerChange(terms: IPickerTerms) {
console.log("Terms", terms);
const tags = terms.map(t => t.name);
this.setState({
selectedTags: tags
});
}
}

View File

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

View File

@ -0,0 +1,10 @@
declare interface ITeamsTaggingWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
TermSetIdFieldLabel: string;
}
declare module 'TeamsTaggingWebPartStrings' {
const strings: ITeamsTaggingWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,47 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.2/MicrosoftTeams.schema.json",
"manifestVersion": "1.2",
"packageName": "TeamsTaggingWebPart",
"id": "0421c2b0-90f0-429f-83e6-f3987e417cae",
"version": "0.1",
"developer": {
"name": "Luis Manez",
"websiteUrl": "https://www.github.com/luismanez",
"privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement",
"termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement"
},
"name": {
"short": "TeamsTagging"
},
"description": {
"short": "Webpart to Tag Teams based on a SharePoint TermSet",
"full": "Webpart to Tag Teams based on a SharePoint TermSet"
},
"icons": {
"outline": "parker_outline.png",
"color": "parker_color.png"
},
"accentColor": "#004578",
"configurableTabs": [
{
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=0421c2b0-90f0-429f-83e6-f3987e417cae%26forceLocale={locale}",
"canUpdateConfiguration": true,
"scopes": [
"team"
]
}
],
"validDomains": [
"*.login.microsoftonline.com",
"*.sharepoint.com",
"*.sharepoint-df.com",
"spoppe-a.akamaihd.net",
"spoprod-a.akamaihd.net",
"resourceseng.blob.core.windows.net",
"msft.spoppe.com"
],
"webApplicationInfo": {
"resource": "https://{teamSiteDomain}",
"id": "00000003-0000-0ff1-ce00-000000000000"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"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-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,
"variable-name": false,
"whitespace": false
}
}