Js public unjoined teams (#779)

* Added a web part that lists all the public teams the current user is not a member of yet.

* Update README.md
This commit is contained in:
Laura Kokkarinen 2019-02-09 12:23:15 +02:00 committed by Vesa Juvonen
parent 3847c3744b
commit 4e4c3ea22f
26 changed files with 18136 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,13 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": true,
"isCreatingSolution": true,
"environment": "spo",
"version": "1.7.1",
"libraryName": "js-public-unjoined-teams",
"libraryId": "f04aba32-b056-47c4-811b-6f17d94cc8d9",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,51 @@
# Public teams I'm not a member of
## Summary
This web part lists all the public teams the current user is not yet a member of. They can then join any of those teams by clicking the button right next to the team name. This web part can also be added to Teams as a tab (built with the 1.7.1 plusbeta/preview version).
![picture of the web part in action](./assets/js-public-unjoined-teams.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.7.1-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
To be able to pin this web part as a Teams tab, your tenant currently needs to be in the targeted release mode. If your tenant is not in targeted release, you can still use the web part in SharePoint as usual.
## Solution
Solution|Author(s)
--------|---------
js-public-unjoined-teams | Laura Kokkarinen ([laurakokkarinen.com](https://laurakokkarinen.com), [@LauraKokkarinen](https://twitter.com/LauraKokkarinen))
## Version history
Version|Date|Comments
-------|----|--------
1.0|February 5, 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 bundle --ship`
- `gulp package solution --ship`
- Deploy the solution package under \sharepoint\solution to the SharePoint app catalog
- Approve the required Microsoft Graph permissions in the SharePoint admin center (Preview, API management)
## Features
This web part lists all the public teams the current user is not yet a member of. They can then join any of those teams by clicking the button right next to the team name. The Teams client does not present this kind of a complete list by default, so the web part is particularly handy to all end users who might not even be aware of all the public teams that are available. This web part can also be added to Teams as a tab. The web part colors are based on the site color theme.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/js-public-unjoined-teams" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 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": {
"public-unjoined-teams-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/publicUnjoinedTeams/PublicUnjoinedTeamsWebPart.js",
"manifest": "./src/webparts/publicUnjoinedTeams/PublicUnjoinedTeamsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"PublicUnjoinedTeamsWebPartStrings": "lib/webparts/publicUnjoinedTeams/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": "js-public-unjoined-teams",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "js-public-unjoined-teams-client-side-solution",
"id": "f04aba32-b056-47c4-811b-6f17d94cc8d9",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Directory.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Group.ReadWrite.All"
}
]
},
"paths": {
"zippedPackage": "solution/js-public-unjoined-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 -->"
}

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,31 @@
{
"name": "js-public-unjoined-teams",
"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.7.1-plusbeta",
"@microsoft/sp-webpart-base": "1.7.1-plusbeta",
"@microsoft/sp-lodash-subset": "1.7.1-plusbeta",
"@microsoft/sp-office-ui-fabric-core": "1.7.1-plusbeta",
"@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.7.1-plusbeta",
"@microsoft/sp-tslint-rules": "1.7.1-plusbeta",
"@microsoft/sp-module-interfaces": "1.7.1-plusbeta",
"@microsoft/sp-webpart-workbench": "1.7.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 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "62eb4ba8-6014-4bd8-a428-097ff64f01c6",
"alias": "PublicUnjoinedTeamsWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "Public Unjoined Teams" },
"description": { "default": "Lists all the public teams the current user is not a member of, and displays a button for joining." },
"officeFabricIconFontName": "TeamsLogoInverse",
"properties": {
"title": "Public Teams I'm not a member of"
}
}]
}

View File

@ -0,0 +1,36 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.publicUnjoinedTeams {
.title {
@include ms-font-xl;
color: "[theme: neutralSecondary, default: #80539D]";
margin-bottom: 15px;
}
.container {
max-width: 700px;
margin: 0;
}
.row {
@include ms-Grid-row;
padding: 4px 15px;
}
button:disabled {
background-color: $ms-color-neutralLight;
color: $ms-color-black;
cursor: auto;
}
button {
text-decoration: none;
height: 30px;
min-width: 65px;
background-color: $ms-color-themePrimary;
border: none;
color: $ms-color-white;
cursor: pointer;
margin-right: 10px;
}
}

View File

@ -0,0 +1,131 @@
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import { difference, find } from '@microsoft/sp-lodash-subset';
import { MSGraphClient } from '@microsoft/sp-http';
import styles from './PublicUnjoinedTeamsWebPart.module.scss';
import * as strings from 'PublicUnjoinedTeamsWebPartStrings';
export interface IPublicUnjoinedTeamsWebPartProps {
title: string;
}
export default class PublicUnjoinedTeamsWebPart extends BaseClientSideWebPart<IPublicUnjoinedTeamsWebPartProps> {
public async render(): Promise<any> {
var unjoinedTeams = await this.getUnjoinedTeams();
this.domElement.innerHTML = `
<div class="${ styles.publicUnjoinedTeams }">
<div class="${styles.title}">
${this.properties.title}
</div>
<div id="container" class="${styles.container}">
<!-- content gets appended here -->
</div>
</div>`;
await this.renderTeams(unjoinedTeams);
}
protected async getUnjoinedTeams(): Promise<any> {
var joinedTeamIds:Array<string> = (await this.graphGet(`https://graph.microsoft.com/beta/me/joinedTeams`)).value.map(x => x.id);
var allTeams= (await this.graphGet(`https://graph.microsoft.com/beta/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&$top=999`)).value;
var publicTeamIds:Array<string> = allTeams.filter(team => team.visibility === 'Public').map(x => x.id);
var missingTeamIds:Array<string> = difference(publicTeamIds, joinedTeamIds);
let missingTeams = JSON.parse('{"value": []}');
missingTeamIds.forEach(teamId => {
var team = find(allTeams, {'id': teamId});
missingTeams['value'].push(team);
});
return missingTeams.value;
}
protected async renderTeams(teamsToShow) {
const container: Element = this.domElement.querySelector('#container');
var userId:string = (await this.graphGet("me")).id;
for (var team of teamsToShow)
{
var row:HTMLDivElement = document.createElement("div");
row.className = styles.row;
container.appendChild(row);
var button:HTMLButtonElement = document.createElement("button");
button.id = team.id;
button.innerHTML = "Join";
button.onclick = (e:Event) => this.addMember(e.srcElement, userId);
row.appendChild(button);
var span:HTMLSpanElement = document.createElement("span");
span.innerHTML = ` ${team.displayName}`;
row.appendChild(span);
}
}
protected addMember(source:Element, userId:string) {
var button:HTMLButtonElement = <HTMLButtonElement>source;
button.disabled = true;
button.innerText = "Joined!";
var body:string = `{"@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/${userId}"}`;
this.graphPost(`https://graph.microsoft.com/v1.0/groups/${button.id}/members/$ref`, body);
}
protected async graphGet(url: string, value?:Array<string>) {
return await this.callGraph(url, "GET");
}
protected async graphPost(url: string, body:string) {
return await this.callGraph(url, "POST", body);
}
protected async callGraph(url: string, method:string, body?:string): Promise<any> {
const graphClient:MSGraphClient = await this.context.msGraphClientFactory.getClient();
var response;
if (method.toUpperCase() == "GET") {
response = await graphClient.api(url).get();
}
if (method.toUpperCase() == "POST") {
response = await graphClient.api(url).post(body);
}
return response;
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('title', {
label: strings.TitleFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Lists all the public teams the current user is not a member of, and displays a button for joining.",
"BasicGroupName": "Group Name",
"TitleFieldLabel": "Title"
}
});

View File

@ -0,0 +1,10 @@
declare interface IPublicUnjoinedTeamsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
TitleFieldLabel: string;
}
declare module 'PublicUnjoinedTeamsWebPartStrings' {
const strings: IPublicUnjoinedTeamsWebPartStrings;
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": "PublicUnjoinedTeams",
"id": "62eb4ba8-6014-4bd8-a428-097ff64f01c6",
"version": "0.1",
"developer": {
"name": "SPFx + Teams Dev",
"websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration",
"privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement",
"termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement"
},
"name": {
"short": "PublicUnjoinedTeams"
},
"description": {
"short": "Allows users to join public Teams they are not yet members of.",
"full": "Allows users to join public Teams they are not yet members of."
},
"icons": {
"outline": "tab20x20.png",
"color": "tab96x96.png"
},
"accentColor": "#004578",
"configurableTabs": [
{
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=62eb4ba8-6014-4bd8-a428-097ff64f01c6",
"canUpdateConfiguration": false,
"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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

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