React my teams (#795)

* Add my teams react web part

* update file extension

* add missing api permissions
This commit is contained in:
Joel Rodrigues 2019-03-09 10:39:42 +00:00 committed by Vesa Juvonen
parent 45e81c8a6c
commit b5331ed08d
31 changed files with 19580 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-my-teams/.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,19 @@
{
"@pnp/generator-spfx": {
"framework": "react",
"pnpFramework": "reactjs.plus",
"pnp-libraries": [],
"pnp-vetting": [],
"spfxenv": "spo"
},
"@microsoft/generator-sharepoint": {
"environment": "spo",
"framework": "react",
"isCreatingSolution": true,
"version": "1.7.1",
"libraryName": "my-teams",
"libraryId": "9bf27890-01d8-4041-8030-72831ed570aa",
"packageManager": "npm",
"componentType": "webpart"
}
}

View File

@ -0,0 +1,51 @@
# React My Teams
## Summary
This sample uses Microsoft Graph to list the Teams the current user is a member of. When the user clicks on one of the teams, the web part retrieves information about the default channel (General) and opens it.
The web part can be configured to open the team on the web browser or client app.
![Demo](./assets/Preview.png)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.7.1-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
## Prerequisites
* Office 365 subscription with SharePoint Online licence
* SharePoint Framework [development environment](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) already set up.
## Solution
Solution|Author(s)
--------|---------
react-my-teams|Joel Rodrigues
## Version history
Version|Date|Comments
-------|----|--------
1.0|February 26, 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.**
---
## Minimal Path to Awesome
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
## Features
This Web Part lists all the teams the current user is a member of.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -0,0 +1,18 @@
{
"$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"
}
}

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": "my-teams",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,4 @@
{
"preset": "@voitanos/jest-preset-spfx-react16",
"rootDir": "../src"
}

View File

@ -0,0 +1,31 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "my-teams-client-side-solution",
"id": "9bf27890-01d8-4041-8030-72831ed570aa",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"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/my-teams.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 -->"
}

29
samples/react-my-teams/gulpfile.js vendored Normal file
View File

@ -0,0 +1,29 @@
'use strict';
// check if gulp dist was called
if (process.argv.indexOf('dist') !== -1) {
// add ship options to command call
process.argv.push('--ship');
}
const path = require('path');
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const gulpSequence = require('gulp-sequence');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
// Create clean distrubution package
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
/**
* Custom Framework Specific gulp tasks
*/
build.initialize(gulp);

18901
samples/react-my-teams/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "my-teams",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "./node_modules/.bin/jest --config ./config/jest.config.json",
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
},
"dependencies": {
"@microsoft/sp-core-library": "1.7.1",
"@microsoft/sp-lodash-subset": "1.7.1",
"@microsoft/sp-office-ui-fabric-core": "1.7.1",
"@microsoft/sp-webpart-base": "1.7.1",
"@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.3.2",
"react-dom": "16.3.2"
},
"resolutions": {
"@types/react": "16.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.7.1",
"@microsoft/sp-module-interfaces": "1.7.1",
"@microsoft/sp-tslint-rules": "1.7.1",
"@microsoft/sp-webpart-workbench": "1.7.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"@voitanos/jest-preset-spfx-react16": "^1.1.0",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"gulp-sequence": "^1.0.0",
"jest": "^23.6.0"
}
}

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,5 @@
export interface IChannel {
id: string;
displayName: string;
description: string;
}

View File

@ -0,0 +1,6 @@
export interface ITeam {
id: string;
displayName: string;
description: string;
isArchived: boolean;
}

View File

@ -0,0 +1,3 @@
export interface ITenant {
id: string;
}

View File

@ -0,0 +1,3 @@
export * from './ITenant';
export * from './ITeam';
export * from './IChannel';

View File

@ -0,0 +1,26 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "58468a8f-40f4-4759-a6f8-493a56bdaaf7",
"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,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "My Teams" },
"description": { "default": "List my teams" },
"officeFabricIconFontName": "TeamsLogo",
"properties": {
"openInClientApp": false
}
}]
}

View File

@ -0,0 +1,90 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import * as strings from 'MyTeamsWebPartStrings';
import { MyTeams, IMyTeamsProps } from './components/myTeams';
import { ITenant } from '../../shared/interfaces';
import { MSGraphClient } from '@microsoft/sp-http';
export interface IMyTeamsWebPartProps {
tenantInfo: ITenant;
openInClientApp: boolean;
}
export default class MyTeamsWebPart extends BaseClientSideWebPart<IMyTeamsWebPartProps> {
private _graphClient: MSGraphClient;
public async onInit(): Promise<void> {
this._graphClient = await this.context.msGraphClientFactory.getClient();
// get tenant info if not available yet
if (!this.properties.tenantInfo && this.properties.openInClientApp) {
this.properties.tenantInfo = await this._getTenantInfo();
}
return super.onInit();
}
public async render(): Promise<void> {
const element: React.ReactElement<IMyTeamsProps> = React.createElement(
MyTeams,
{
graphClient: this._graphClient,
tenantId: this.properties.tenantInfo.id,
openInClientApp: this.properties.openInClientApp
}
);
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: [
PropertyPaneToggle('openInClientApp', {
label: strings.OpenInClientAppFieldLabel,
})
]
}
]
}
]
};
}
private _getTenantInfo = async (): Promise<ITenant> => {
let tenant: ITenant = null;
try {
const tenantResponse = await this._graphClient.api('organization').select('id').version('v1.0').get();
tenant = tenantResponse.value as ITenant;
console.log(tenant);
} catch (error) {
console.log('Error getting tenant information');
}
return tenant;
}
}

View File

@ -0,0 +1,7 @@
import { MSGraphClient } from "@microsoft/sp-http";
export interface IMyTeamsProps {
graphClient: MSGraphClient;
tenantId: string;
openInClientApp: boolean;
}

View File

@ -0,0 +1,5 @@
import { ITeam } from "../../../../shared/interfaces";
export interface IMyTeamsState {
items: ITeam[];
}

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.myTeams {
.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,99 @@
import * as React from 'react';
import { FocusZone } from 'office-ui-fabric-react/lib/FocusZone';
import { List } from 'office-ui-fabric-react/lib/List';
import styles from '../myTeams/MyTeams.module.scss';
import { IMyTeamsProps, IMyTeamsState } from '.';
import { escape } from '@microsoft/sp-lodash-subset';
import { ITeam, IChannel } from '../../../../shared/interfaces';
export class MyTeams extends React.Component<IMyTeamsProps, IMyTeamsState> {
private _myTeams: ITeam[] = [];
constructor(props: IMyTeamsProps) {
super(props);
this.state = {
items: []
};
}
public async componentDidMount() {
await this._load();
}
public async componentDidUpdate(prevProps: IMyTeamsProps) {
if (this.props.openInClientApp !== prevProps.openInClientApp) {
await this._load();
}
}
private _load = async (): Promise<void> => {
this._myTeams = await this._getTeams();
this.setState({
items: this._myTeams
});
}
public render(): React.ReactElement<IMyTeamsProps> {
return (
<FocusZone>
<List
className={styles.myTeams}
items={this._myTeams}
renderedWindowsAhead={4}
onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
private _onRenderCell = (team: ITeam, index: number | undefined): JSX.Element => {
return (
<div>
<a href="#" title='Click to open channel' onClick={this._openChannel.bind(this, team.id, this.props.tenantId)}>
<span>{team.displayName}</span>
</a>
</div>
);
}
private _openChannel = async (teamId: string, tenantId: string): Promise<void> => {
let link = '#';
const teamChannels: IChannel[] = await this._getTeamChannels(teamId);
const channel = teamChannels[0];
if (this.props.openInClientApp) {
link = `https://teams.microsoft.com/l/channel/${channel.id}/${channel.displayName}?groupId=${teamId}&tenantId=${tenantId}`;
} else {
link = `https://teams.microsoft.com/_#/conversations/${channel.displayName}?threadId=${channel.id}&ctx=channel`;
}
window.open(link, '_blank');
}
private _getTeams = async (): Promise<ITeam[]> => {
let myTeams: ITeam[] = [];
try {
const teamsResponse = await this.props.graphClient.api('me/joinedTeams').version('v1.0').get();
myTeams = teamsResponse.value as ITeam[];
} catch (error) {
console.log('Error getting teams');
}
return myTeams;
}
private _getTeamChannels = async (teamId): Promise<IChannel[]> => {
let channels: IChannel[] = [];
try {
const channelsResponse = await this.props.graphClient.api(`teams/${teamId}/channels`).version('v1.0').get();
channels = channelsResponse.value as IChannel[];
} catch (error) {
console.log('Error getting channels for team ' + teamId);
}
return channels;
}
}

View File

@ -0,0 +1,3 @@
export * from './IMyTeamsProps';
export * from './IMyTeamsState';
export * from './MyTeams';

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"OpenInClientAppFieldLabel": "Open link in client app (open in browser by disabled)"
}
});

View File

@ -0,0 +1,10 @@
declare interface IMyTeamsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
OpenInClientAppFieldLabel: string;
}
declare module 'MyTeamsWebPartStrings' {
const strings: IMyTeamsWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"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
}
}