Merge pull request #1293 from siddharth-vaghasia/react-teams-message

This commit is contained in:
Hugo Bernier 2020-05-28 18:44:56 -04:00 committed by GitHub
commit 52c5c06b3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 18525 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-message/.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.10.0",
"libraryName": "react-teams-message",
"libraryId": "13a092c0-6eb6-46b0-bbc2-17e2ae11adbe",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,118 @@
---
page_type: sample
products:
- office-365
- office-sp
languages:
- javascript
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
platforms:
- React
createdDate: 1/1/2016 12:00:00 AM
---
# Web part showing Current User's MS Teams and send message.
## Summary
This is a sample web part that displays currently logged in user's Microsoft Teams(user is member of), its channels and will allow sending messages to any Team's channel. It will also allow the user to open the team's channel via the link. This web part can be useful on the Intranet home page which can be added as My Teams web part.
* Web Part in Action
![WebPart in Action](./assets/myteamsmessage.gif)
* Configurable Web Part Properties
![Web Part Properties](./assets/webpartproperties.jpg)
## Features
* Show Current logged in user's Teams in Tree View
* On Expanding any Team, it will show selected team's channels.
* Message can send to any Team's channel by either by selecting any channel(configurable as web part properties)
* Context menu for every channel to 1) To open channel's link in Teams. 2) To send message to team.
* A dialog box to send message.
Configurable Web part Properties
* Web Part Title to be displayed On top of tree view (like My Teams).
* Toggle to Show/hide Teams and channel's Description.
* Toggle On/Off whether to open send message popup should open soon as a channel is selected.
* Please refer this [link](https://www.c-sharpcorner.com/article/microsoft-teams-operations-in-spfx-webpart-using-graph-api/) if you are interested in learning step by step on how to call Team graph API from SPFx web part.
## Used SharePoint Framework Version
![1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Prerequisites
* SharePoint Online tenant
* You have provided permission in SharePoint admin for accessing Graph API on behalf of your solution. We can do it before deployment as proactive steps, or after deployment. You can refer to [steps about how to do this post-deployment](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aad-tutorial#deploy-the-solution-and-grant-permissions). Basically you have to use API Access Page of SharePoint admin and add below permission for our use case.
```
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite.All"
},
{
"resource": "Microsoft Graph",
"scope": "Group.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Group.ReadWrite.All"
}
]
```
## Concepts
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* Using react framework in SPFx webpart
* Calling Teams Graph API in SPFx webpart
* Usage of PnP Tree View Control
* Usage of Fluent UI/Office UI Fabric Controls
## Solution
Solution|Author(s)
--------|---------
react-teams-message | Siddharth Vaghasia(@siddh_me)
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|May 23, 2020|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.**
---
## Minimal Path to Awesome
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-teams-message" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 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": {
"my-teams-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/myTeams/MyTeamsWebPart.js",
"manifest": "./src/webparts/myTeams/MyTeamsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"MyTeamsWebPartStrings": "lib/webparts/myTeams/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-message",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,32 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-teams-message-client-side-solution",
"id": "13a092c0-6eb6-46b0-bbc2-17e2ae11adbe",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite.All"
},
{
"resource": "Microsoft Graph",
"scope": "Group.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Group.ReadWrite.All"
}
]
},
"paths": {
"zippedPackage": "solution/react-teams-message.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 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(require('gulp'));

17687
samples/react-teams-message/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "react-teams-message",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/graph": "^2.0.4",
"@pnp/sp": "^2.0.4",
"@pnp/spfx-controls-react": "1.18.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

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,56 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { MSGraphClient } from "@microsoft/sp-http";
export class ServiceProvider {
public _graphClient: MSGraphClient;
private spcontext: WebPartContext;
public constructor(spcontext: WebPartContext) {
this.spcontext = spcontext;
}
public getmyTeams = async (): Promise<[]> => {
this._graphClient = await this.spcontext.msGraphClientFactory.getClient(); //TODO
let myTeams: [] = [];
try {
const teamsResponse = await this._graphClient.api('me/joinedTeams').version('v1.0').get();
myTeams = teamsResponse.value as [];
} catch (error) {
console.log('Unable to get teams', error);
}
return myTeams;
}
public getChannel = async (teamID): Promise<[]> => {
this._graphClient = await this.spcontext.msGraphClientFactory.getClient(); //TODO
let myTeams: [] = [];
try {
const teamsResponse = await this._graphClient.api('teams/' + teamID + '/channels').version('v1.0').get();
myTeams = teamsResponse.value as [];
} catch (error) {
console.log('unable to get channels', error);
}
return myTeams;
}
public sendMessage = async (teamId, channelId, message): Promise<[]> => {
this._graphClient = await this.spcontext.msGraphClientFactory.getClient();
try {
// https://graph.microsoft.com/beta/teams/{team-id}/channels/{channel-id}/messages
var content = {
"body": {
"content": message
}
};
const messageResponse = await this._graphClient.api('/teams/' + teamId + '/channels/' + channelId + "/messages/")
.version("beta").post(content);
return messageResponse;
} catch (error) {
console.log('Unable to send message', error);
return null;
}
}
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "a540a9b2-82b3-466a-83b7-7caeeb0e464d",
"alias": "MyTeamsWebPart",
"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,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "myTeams" },
"description": { "default": "myTeams description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "myTeams"
}
}]
}

View File

@ -0,0 +1,72 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneToggle,
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'MyTeamsWebPartStrings';
import MyTeams from './components/MyTeams';
import { IMyTeamsProps } from './components/IMyTeamsProps';
export interface IMyTeamsWebPartProps {
WebpartTitle: string;
ShowDescription:boolean;
OpenPopupOnSelectingChannel:boolean;
}
export default class MyTeamsWebPart extends BaseClientSideWebPart <IMyTeamsWebPartProps> {
public render(): void {
const element: React.ReactElement<IMyTeamsProps> = React.createElement(
MyTeams,
{
context: this.context,
webparttitle:this.properties.WebpartTitle,
showdescription:this.properties.ShowDescription,
openpopuponselectingchannel:this.properties.OpenPopupOnSelectingChannel
}
);
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('WebpartTitle', {
label: "WebPart Heading"
}),
PropertyPaneToggle('ShowDescription', {
label: "Show Description"
}),
PropertyPaneToggle('OpenPopupOnSelectingChannel', {
label: "Open Popup Selecting Channel"
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,8 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IMyTeamsProps {
context: WebPartContext;
webparttitle: string;
showdescription:boolean;
openpopuponselectingchannel:boolean;
}

View File

@ -0,0 +1,37 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.myTeams {
label{
font-size: 16px;
line-height: 1.5;
}
.radio{
width: 16px;
height: 16px;
}
.textbox{
height: 30px;
width: 300px;
}
h4{
font-style: italic;
font-weight: normal;
}
.buttons{
background-color: $ms-color-themePrimary;
width: 200px;
height: 40px;
color: white;
font-size: 18px;
}
.webpartitle {
color: $ms-color-themePrimary;
margin: 0px;
}
}

View File

@ -0,0 +1,237 @@
import * as React from 'react';
import styles from './MyTeams.module.scss';
import { IMyTeamsProps } from './IMyTeamsProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { ServiceProvider } from '../../../shared/services/ServiceProvider';
import { TreeView, ITreeItem, TreeItemActionsDisplayMode, TreeViewSelectionMode } from "@pnp/spfx-controls-react/lib/TreeView";
import { PrimaryButton, DefaultButton, Dialog, DialogFooter, DialogType, TextField, MessageBar,MessageBarType } from 'office-ui-fabric-react';
import { initializeIcons } from '@uifabric/icons';
export interface IMyTeamsState {
myteams: any[];
selectedTeam: any;
teamChannels: any;
selectedChannel: any;
hideDailog: boolean;
currentMessage: string;
showMessage: boolean;
}
export default class MyTeams extends React.Component<IMyTeamsProps, IMyTeamsState> {
private serviceProvider;
private selectedItems;
public constructor(props: IMyTeamsProps, state: IMyTeamsState) {
super(props);
this.serviceProvider = new ServiceProvider(this.props.context);
this.state = {
myteams: [],
selectedTeam: null,
selectedChannel: null,
teamChannels: [],
hideDailog: true,
currentMessage: "",
showMessage: false
};
initializeIcons();
}
public async componentDidMount() {
this.GetmyTeams();
}
private GetmyTeams() {
this.serviceProvider.
getmyTeams()
.then(
(result: any[]): void => {
console.log(result);
this.createRequiredTreeItems(result);
}
)
.catch(error => {
console.log(error);
});
}
private async createRequiredTreeItems(result) {
var array = [];
let i = 0;
result.forEach(element => {
array.push({
index: i,
key: element.id,
label: element.displayName,
subLabel: this.props.showdescription ? element.description : "",
data: element,
type: "team",
selectable: false,
children: [{ key: element.id + "_d", label: "Loading channels" }]
});
i++;
});
this.setState({ myteams: array });
}
private async loadChannels(teamid, index) {
var array = this.state.myteams;
var firstTeamChannels = await this.getChannels(teamid);
array[index].children = [];
firstTeamChannels.forEach(channel => {
array[index].children.push({
key: channel.id,
label: channel.displayName,
subLabel: this.props.showdescription ? channel.description : "",
data: channel,
type: "channel",
parent: array[index],
iconProps: {
iconName: 'TeamsLogoInverse'
},
actions: [{
title: "Open Channel",
iconProps: {
iconName: 'Link'
},
id: "GetItem",
actionCallback: async (treeItem: ITreeItem) => {
console.log(treeItem.data.webUrl);
window.open(treeItem.data.webUrl);
}
},
{
title: "Send Message",
iconProps: {
iconName: 'Send'
},
id: "Send Message",
actionCallback: async (treeItem: ITreeItem) => {
this.selectedItems = [treeItem];
this.setState({ hideDailog: false });
}
}]
});
});
this.setState({ myteams: array });
}
public render(): React.ReactElement<IMyTeamsProps> {
return (
<React.Fragment>
<div className={styles.myTeams}>
<i className="ms-Icon ms-Icon--TeamsLogo" aria-hidden="true"></i>
<h1 className={styles.webpartitle}>
{this.props.webparttitle}</h1>
{this.state.showMessage &&
<React.Fragment>
<br></br>
< MessageBar onDismiss={()=>{this.setState({showMessage:false});}} style={{height:'20px'}} messageBarType={MessageBarType.success} >
Message Posted Successfully.
</MessageBar >
</React.Fragment>
}
<TreeView
items={this.state.myteams}
defaultExpanded={false}
selectChildrenIfParentSelected={false}
selectionMode={TreeViewSelectionMode.Single}
showCheckboxes={false}
treeItemActionsDisplayMode={TreeItemActionsDisplayMode.ContextualMenu}
onSelect={(items) => this.onTreeItemSelect(items)}
onExpandCollapse={(items, isExpanded) => this.onTreeItemExpandCollapse(items, isExpanded)}
/>
{!this.state.hideDailog &&
<Dialog
hidden={this.state.hideDailog}
onDismiss={this._closeDialog}
dialogContentProps={{
type: DialogType.largeHeader,
title: this.selectedItems[0].parent.label
}}
modalProps={{
isBlocking: false,
styles: { main: { minWidth: 600 } }
}}
>
<span>{"Sending to channel: " + this.selectedItems[0].label}</span>
<TextField required onChange={evt => this.updateInputValue(evt)} value={this.state.currentMessage} label="Message" multiline resizable={true} />
<DialogFooter>
<PrimaryButton onClick={() => this._sendMessage()} text="Send" />
<DefaultButton onClick={this._closeDialog} text="Cancel" />
</DialogFooter>
</Dialog>
}
</div>
</React.Fragment >
);
}
private onTreeItemSelect(items: ITreeItem[]) {
console.log("Items selected: ", items);
if (this.props.openpopuponselectingchannel) {
this.selectedItems = items;
this.setState({ hideDailog: false });
}
}
private async onTreeItemExpandCollapse(item: any, isExpanded: boolean) {
console.log((isExpanded ? "Item expanded: " : "Item collapsed: ") + item);
this.loadChannels(item.data.id, item.index);
}
private updateInputValue(evt) {
this.setState({
currentMessage: evt.target.value
});
}
private _closeDialog = (): void => {
this.setState({ hideDailog: true });
}
private async getChannels(teamid) {
var returnResult = await this.serviceProvider.
getChannel(teamid)
.then(
(result): void => {
console.log(result);
return result;
}
)
.catch(error => {
console.log(error);
});
return returnResult;
}
private async _sendMessage() {
var selecteTeamId = this.selectedItems[0].parent.key;
var selectedChannelId = this.selectedItems[0].key;
await this.serviceProvider.
sendMessage(selecteTeamId, selectedChannelId, this.state.currentMessage)
.then(
(result: any[]): void => {
console.log(result);
this.setState({ hideDailog: true, currentMessage: "",showMessage:true });
}
)
.catch(error => {
console.log(error);
});
}
}

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 IMyTeamsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'MyTeamsWebPartStrings' {
const strings: IMyTeamsWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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
}
}