Dev - SPFx Sample, Yammer REST API SPFx react webpart (#182)

* react-yammer-api initial commit. SPFx Yammer search webpart sample. It uses the Yammer JavaScript SDK and the Yammer REST APIs.

* Try catch added to _iframeAuthentication. if Office 365 Identity Enforcement  is not enabled, will promise reject instead of throwing exception.
README updated.

* README texts updated

* README texts updated
This commit is contained in:
Velin Georgiev 2017-05-06 16:40:10 +01:00 committed by Vesa Juvonen
parent c4e72f1584
commit d3bf47845b
37 changed files with 1074 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 @@
* text=auto

32
samples/react-yammer-api/.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,14 @@
# Folders
.vscode
coverage
node_modules
sharepoint
src
temp
# Files
*.csproj
.git*
.yo-rc.json
gulpfile.js
tsconfig.json

View File

@ -0,0 +1,42 @@
// Use Chrome browser to debug SharePoint Framework React webpart with Visual Studio Code on Windows
// - Install "Debugger for Chrome" extension
// - Add this configuration to the launch.json
// - Close all Chrome browser active instances
// - Start the SPFx nodejs server by executing "gulp serve"
// - Go to VS Code debug view (top menu -> View -> Debug)
// - Hit the play (start debugging) button
// Happy debugging!
// Full guide here http://blog.velingeorgiev.pro/how-debug-sharepoint-framework-webpart-visual-studio-code
{
"version": "0.2.0",
"configurations": [
{
"name": "SPFx Local",
"type": "chrome",
"request": "launch",
"url": "https://localhost:4321/temp/workbench.html",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///../../../../../*": "${webRoot}/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222"
]
},
{
"name": "SPFx Online Workbench",
"type": "chrome",
"request": "launch",
"url": "https://<your_tenant>.sharepoint.com/sites/<your_site>/_layouts/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///../../../../../*": "${webRoot}/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222"
]
}
]
}

View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "./node_modules/typescript/lib"
}

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"libraryName": "react-yammer-api",
"framework": "react",
"version": "1.0.0",
"libraryId": "9e199a26-c09f-4071-9902-ee8e40e80f16"
}
}

View File

@ -0,0 +1,94 @@
# Yammer REST API SPFx webpart #
## Summary
This sample shows how Yammer REST APIs can be consumed by using SharePoint Framework React webpart and the Yammer JavaScript SDK. The SPFx webpart contains wrapper around the Yammer JavaScript SDK that can be extended for fluent typescript api experience.
### Yammer search
Sample SharePoint Framework client-side web part built using React that consumes the Yammer search REST API.
![The yammer search web part displayed in SharePoint online](./assets/spfx-yammer-api-webpart.jpg)
### Smart Authentication, if Yammer Enforce Office 365 identity is enabled
If Yammer Office 365 Identity Enforcement is enabled, the webpart will 'smart' authenticate Office 365 user when in SharePoint Online environment i.e. a user should allow the app (consent popup) once in a lifetime. After, the user will be logged in all the time. Smart because if you do not have the yammer auth cookies, you would not have to re-authenticate with login button and popups.
To enable Office 365 Identity Enforcement on `Office 365 Enterprise E3 Trial tenant`, go to the Office 365 admin -> Admin centers -> Yammer -> Security Settings -> Enforce Office 365 identity.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-GA-green.svg)
## Applies to
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 Enterprise E3](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
** [Office 365 Enterprise E3 Trial](https://products.office.com/en-ie/business/office-365-enterprise-e3-business-software) instead of `Office 365 Enterprise E3 Developer Trial` is required to test the webpart with Yammer.
## Prerequisites
- Office 365 subscription with SharePoint Online and Yammer.
- SharePoint Framework [development environment](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) already set up.
- Yammer app already registered. Here is a [how to register an app with Yammer](https://developer.yammer.com/docs/app-registration) guide.
## Solution
Solution|Author(s)
--------|---------
react-yammer-api | Velin Georgiev ([@VelinGeorgiev](https://twitter.com/velingeorgiev)), Joseph King ([@7kingjoe3](https://twitter.com/7kingjoe3))
## Version history
Version|Date|Comments
-------|----|--------
0.0.1|April 19, 2017 | Initial commit
## 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 Yammer corresponding to your Office 365 tenant, register a new Yammer App. Here is a [how to register an app with Yammer](https://developer.yammer.com/docs/app-registration) guide.
- Do not forget to paste your Office 365 tenant url in the `Javascript Origins` upon Yammer app registration e.g `Javascript Origins: https://<your_tenant>.sharepoint.com`.
- Add Yammer app redirect URI e.g. `https://<your_tenant>.sharepoint.com/SitePages/Home.aspx`.
- Make sure the Yammer app is enabled
![Yammer app enabled](./assets/yammer-enabled-screen.png)
- Copy the Yammer app client Id and redirect uri.
- Go to the SPFx webpart folder and find **src/webparts/reactYammerApi/yammer/ProdConfiguration.ts**.
- Replace the config client id and redirect uri with the copied from the yammer registered app values.
```typescript
import { IConfiguration } from './IConfiguration';
/**
* Webpart production configuration.
*/
export class ProdConfiguration implements IConfiguration {
public readonly clientId: string = "<YOUR_YAMMER_APP_CLIENT_ID>";
public readonly redirectUri: string = "<YOUR_YAMMER_APP_REDIRECT_URI>";
}
```
- Open the command line, navigate to the web part folder and execute:
- `npm i`
- `gulp test` (optional)
- `gulp serve --nobrowser`
- Navigate to the hosted version of the SharePoint workbench. (`https://<your_tenant>.sharepoint.com/sites/<your_site>/_layouts/15/workbench.aspx`).
- Add the **React Yammer API** web part.
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- Using React for building SharePoint Framework client-side web parts.
- Using Office UI Fabric React styles for building user experience consistent with SharePoint and Office.
- On-demand authentication with Yammer using the Yammer JavaScript SDK.
- Communicating with Yammer using its REST APIs.
- Passing web part properties to React components.
- Passing localized strings to React components.
- Unit tests including spies, mocks and faking class methods and properties with stubs.
![](https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/yammer-rest-api)

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,13 @@
{
"entries": [
{
"entry": "./lib/webparts/reactYammerApi/ReactYammerApiWebPart.js",
"manifest": "./src/webparts/reactYammerApi/ReactYammerApiWebPart.manifest.json",
"outputPath": "./dist/react-yammer-api.bundle.js"
}
],
"externals": {},
"localizedResources": {
"reactYammerApiStrings": "webparts/reactYammerApi/loc/{locale}.js"
}
}

View File

@ -0,0 +1,3 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,6 @@
{
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-yammer-api",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,10 @@
{
"solution": {
"name": "react-yammer-api-client-side-solution",
"id": "9e199a26-c09f-4071-9902-ee8e40e80f16",
"version": "1.0.0.0"
},
"paths": {
"zippedPackage": "solution/react-yammer-api.sppkg"
}
}

View File

@ -0,0 +1,9 @@
{
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,46 @@
{
// 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-unused-imports": 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,
"prefer-const": true
}
}
}

View File

@ -0,0 +1,3 @@
{
"cdnBasePath": "<!-- PATH TO CDN -->"
}

6
samples/react-yammer-api/gulpfile.js vendored Normal file
View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

View File

@ -0,0 +1,37 @@
{
"name": "react-yammer-api",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-client-base": "~1.0.0",
"@microsoft/sp-core-library": "~1.0.0",
"@microsoft/sp-webpart-base": "~1.0.0",
"@types/react": "0.14.46",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"react": "15.4.2",
"react-dom": "15.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.0.0",
"@microsoft/sp-module-interfaces": "~1.0.0",
"@microsoft/sp-webpart-workbench": "~1.0.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"@types/sinon": "^1.16.36",
"enzyme": "~2.8.0",
"react-addons-test-utils": "~15.4.2"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
}
}

View File

@ -0,0 +1,3 @@
export interface IReactYammerApiWebPartProps {
defaultSearchQuery: string;
}

View File

@ -0,0 +1,20 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "d9ec2b3c-849b-44d1-bb4d-baa3528f47ab",
"alias": "ReactYammerApiWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [{
"groupId": "d9ec2b3c-849b-44d1-bb4d-baa3528f47ab",
"group": { "default": "Under Development" },
"title": { "default": "React Yammer API" },
"description": { "default": "Uses the Yammer JavaScript SDK to interact with the Yammer REST APIs." },
"officeFabricIconFontName": "Page",
"properties": {
"description": "React Yammer API"
}
}]
}

View File

@ -0,0 +1,63 @@
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 'reactYammerApiStrings';
import ReactYammerApi from './components/ReactYammerApi';
import { IReactYammerApiProps } from './components/IReactYammerApiProps';
import { IReactYammerApiWebPartProps } from './IReactYammerApiWebPartProps';
import { IConfiguration } from './yammer/IConfiguration';
import { ProdConfiguration } from './yammer/ProdConfiguration';
import { IYammerProvider } from './yammer/IYammerProvider';
import YammerProvider from './yammer/YammerProvider';
export default class ReactYammerApiWebPart extends BaseClientSideWebPart<IReactYammerApiWebPartProps> {
public render(): void {
let config: IConfiguration = new ProdConfiguration();
let yammerProvider: IYammerProvider = new YammerProvider(config);
const element: React.ReactElement<IReactYammerApiProps > = React.createElement(
ReactYammerApi,
{
yammer: yammerProvider,
defaultSearchQuery: this.properties.defaultSearchQuery,
strings: strings
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneSearchOptions
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('defaultSearchQuery', {
label: strings.DefaultSearchQueryFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,7 @@
import { IYammerProvider } from '../yammer/IYammerProvider';
export interface IReactYammerApiProps {
yammer: IYammerProvider;
defaultSearchQuery: string;
strings: IReactYammerApiStrings;
}

View File

@ -0,0 +1,6 @@
import { SearchResult } from '../yammer/SearchResult';
export interface IReactYammerApiState {
searchResults: Array<SearchResult>;
searchQuery: string;
}

View File

@ -0,0 +1,52 @@
.helloWorld {
.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 {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: #0078d7;
border-color: #0078d7;
color: #ffffff;
// 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: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,107 @@
import * as React from 'react';
import { IReactYammerApiProps } from './IReactYammerApiProps';
import { IReactYammerApiState } from './IReactYammerApiState';
import { Button } from 'office-ui-fabric-react/lib/Button';
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
import { SearchResult } from '../yammer/SearchResult';
export default class ReactYammerApi extends React.Component<IReactYammerApiProps, IReactYammerApiState> {
constructor(props: IReactYammerApiProps) {
super(props);
this.state = {
searchResults: new Array<SearchResult>(),
searchQuery: this.props.defaultSearchQuery
} as IReactYammerApiState;
}
public componentDidMount(): void {
// load the Yammer Sdk and authenticate.
this.props.yammer.loadSdk().then(_ => {
this.props.yammer
.getLoginStatus()
.then((res: any) => {
// search based on the default search query.
// set in the in the webpart properties pane.
this._search(this.props.defaultSearchQuery);
})
.catch((err: any) => {
// add login button if authentication failed.
this.props.yammer.loginButton("#yammer-login");
});
});
}
public render(): React.ReactElement<IReactYammerApiProps> {
return (
<div className="ms-Grid">
<div className="ms-Grid-row">
<h1 className="ms-Grid-col ms-u-sm12">
Yammer {this.props.strings.SearchLabel}
</h1>
<div className="ms-Grid-col ms-u-sm10">
<SearchBox
className="search-box"
value={this.state.searchQuery}
onChange={this._handleInputChange.bind(this)}
onSearch={this._handleSearch.bind(this)}>
</SearchBox>
</div>
<div className="ms-Grid-col ms-u-sm2">
<Button id="SearchButton" onClick={this._handleSearch.bind(this)}>
{this.props.strings.SearchLabel}
</Button>
</div>
<div className="ms-Grid-col ms-u-sm12">
<code>
{this.state.searchResults.map(item =>
<pre key={item.id}>
{JSON.stringify(item, null, 2)}
</pre>
)}
</code>
</div>
<div className="ms-Grid-col ms-u-sm12">
<span id="yammer-login"></span>
</div>
</div>
</div>
);
}
/**
* Performs Yammer search.
* @param querySearch
*/
private _search(querySearch: string): void {
this.props.yammer.search(querySearch)
.then((searchResults: Array<SearchResult>) => {
this.setState((prevState: IReactYammerApiState, props: IReactYammerApiProps): IReactYammerApiState => {
prevState.searchResults = searchResults;
return prevState;
});
})
.catch((err) => {
console.log(err);
});
}
/**
* Search button event handler.
* @param event
*/
private _handleSearch(event: any): void {
this._search(this.state.searchQuery);
}
/**
* Search input handler.
* @param searchQuery
*/
private _handleInputChange(searchQuery: string): void {
this.setState((prevState: IReactYammerApiState, props: IReactYammerApiProps): IReactYammerApiState => {
prevState.searchQuery = searchQuery;
return prevState;
});
}
}

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"PropertyPaneSearchOptions": "Search Options",
"BasicGroupName": "Group Name",
"DefaultSearchQueryFieldLabel": "Default Search Query",
"SearchLabel": "Search"
}
});

View File

@ -0,0 +1,11 @@
declare interface IReactYammerApiStrings {
PropertyPaneSearchOptions: string;
BasicGroupName: string;
DefaultSearchQueryFieldLabel: string;
SearchLabel: string;
}
declare module 'reactYammerApiStrings' {
const strings: IReactYammerApiStrings;
export = strings;
}

View File

@ -0,0 +1,164 @@
/// <reference types="mocha" />
/// <reference types="sinon" />
import * as React from 'react';
import { assert, expect } from 'chai';
import { mount } from 'enzyme';
import ReactYammerApi from '../components/ReactYammerApi';
import { IConfiguration } from '../yammer/IConfiguration';
import { IYammerProvider } from '../yammer/IYammerProvider';
import YammerProvider from '../yammer/YammerProvider';
import { SearchResult } from '../yammer/SearchResult';
declare const sinon: any;
describe("ReactYammerApiWebPart:", () => {
let componentDidMountSpy: any;
let yammerComponent: any;
let yammerProvider: IYammerProvider;
let componentSearchMethodSpy: any;
let componentSearchButtonHandlerSpy: any;
let componentSearchInputHandlerSpy: any;
let yammerProviderLoginButtonStub: any;
let yammerProviderGetLoginStatusStub: any;
let yammerProviderSearchStub: any;
// define fake data.
let fakeDefaultSearchQuery: string = "#joined";
let fakeConfig: IConfiguration = { clientId: "fakeId", redirectUri: "fakeUri" } as IConfiguration;
let fakeStrings: { SearchLabel: string } = { SearchLabel: "fakeSearchLabel" };
let fakeSearchResults: Array<SearchResult> = [{ id: 1, url: "fakeUrl", text: "fakeText" }] as Array<SearchResult>;
before(() => {
// set object stubs of the Yammer config and provider classes.
let configStub: IConfiguration = sinon.stub(fakeConfig);
// stub the fake strings.
let stringsStub: any = sinon.stub(fakeStrings);
// fake Yammer provider methods.
let searchResults: Array<SearchResult> = sinon.stub(fakeSearchResults);
yammerProviderSearchStub = sinon.stub(YammerProvider.prototype, "search");
yammerProviderSearchStub.returns(new Promise((resolve, reject) => resolve(searchResults)));
sinon.stub(YammerProvider.prototype, "loadSdk").returns(new Promise((resolve, reject) => resolve()));
yammerProviderGetLoginStatusStub = sinon.stub(YammerProvider.prototype, "getLoginStatus");
yammerProviderGetLoginStatusStub.returns(new Promise((resolve, reject) => resolve({})));
yammerProviderLoginButtonStub = sinon.stub(YammerProvider.prototype, "loginButton");
yammerProviderLoginButtonStub.returns(new Promise((resolve, reject) => resolve({})));
// init the Yammer provider.
yammerProvider = new YammerProvider(configStub);
// set spies on the Yammer Component methods. Should be added before mount.
componentSearchMethodSpy = sinon.spy(ReactYammerApi.prototype, "_search");
componentSearchButtonHandlerSpy = sinon.spy(ReactYammerApi.prototype, "_handleSearch");
componentSearchInputHandlerSpy = sinon.spy(ReactYammerApi.prototype, "_handleInputChange");
componentDidMountSpy = sinon.spy(ReactYammerApi.prototype, "componentDidMount");
// mount the component.
yammerComponent =
mount(<ReactYammerApi defaultSearchQuery={fakeDefaultSearchQuery} yammer={yammerProvider} strings={stringsStub} />);
});
after(() => {
componentDidMountSpy.restore();
componentSearchButtonHandlerSpy.restore();
componentSearchInputHandlerSpy.restore();
componentSearchMethodSpy.restore();
});
it("Should call componentDidMount only once", () => {
// check if the componentDidMount is called once.
expect(componentDidMountSpy.calledOnce).to.be.true;
});
it("Should render title", () => {
// check if the correct hreader is present.
expect(yammerComponent.find("h1").text()).to.be.equals(`Yammer ${fakeStrings.SearchLabel}`);
});
it("Should have one 'code' element displayed", () => {
// check if only one HTML code element is present on render.
expect(yammerComponent.find("code").length).to.be.equals(1);
});
it("Should not display 'pre' elements when initialy loaded", () => {
expect(yammerComponent.find("pre").length).to.be.equals(0);
});
it("Should have correct initial defaultSearchQuery property", () => {
expect(yammerComponent.props().defaultSearchQuery).to.be.equals(fakeDefaultSearchQuery);
});
it("Should not call yammer button since logged in", () => {
expect(yammerProviderLoginButtonStub.called).to.be.false;
});
it("Should call component _search", (done) => {
setTimeout(() => {
expect(componentSearchMethodSpy.calledOnce).to.be.true;
done();
}, 50);
});
it("Should call yammer search after component _search", (done) => {
setTimeout(() => {
expect(yammerProviderSearchStub.calledOnce).to.be.true;
expect(yammerProviderSearchStub.calledAfter(componentSearchMethodSpy)).to.be.true;
done();
}, 50);
});
it("Should load one search result", (done) => {
setTimeout(() => {
let l: number = yammerComponent.state("searchResults").length;
expect(l).to.be.equals(1);
done();
}, 50);
});
it("Should load search result with correct data", (done) => {
setTimeout(() => {
let results: Array<SearchResult> = yammerComponent.state("searchResults");
if (results.length === 0) {
assert.fail();
} else {
expect(results[0].id).to.be.equals(fakeSearchResults[0].id);
expect(results[0].url).to.be.equals(fakeSearchResults[0].url);
expect(results[0].text).to.be.equals(fakeSearchResults[0].text);
}
done();
}, 50);
});
it("Should call the button click handler", () => {
yammerComponent.find("#SearchButton").simulate("click");
expect(componentSearchButtonHandlerSpy.calledOnce).to.be.true;
expect(componentSearchMethodSpy.calledTwice).to.be.true;
expect(componentSearchMethodSpy.calledAfter(componentSearchButtonHandlerSpy)).to.be.true;
});
it("Should call the input change handler", () => {
yammerComponent.find(".search-box input").simulate("change");
expect(componentSearchInputHandlerSpy.called).to.be.true;
expect(componentSearchInputHandlerSpy.getCalls(0)[0].args[0]).to.be.equals("#joined");
});
it("Should call login button if not logged in", (done) => {
// change get login status to rejected promise.
yammerProviderGetLoginStatusStub.returns(new Promise((resolve, reject) => reject({})));
// mount the component to simulate getLoginStatus failure.
mount(<ReactYammerApi defaultSearchQuery={fakeDefaultSearchQuery} yammer={yammerProvider} strings={sinon.stub(fakeStrings)} />);
setTimeout(() => {
expect(yammerProviderLoginButtonStub.calledOnce).to.be.true;
done();
}, 50);
});
});

View File

@ -0,0 +1,13 @@
/**
* Webpart configuration.
*/
export interface IConfiguration {
/**
* Yammer application client id.
*/
clientId: string;
/**
* Yammer application redirect uri.
*/
redirectUri: string;
}

View File

@ -0,0 +1,27 @@
import { SearchResult } from './SearchResult';
/**
* Yammer rest apis provider interface.
*/
export interface IYammerProvider {
/**
* Appends the Yammer platform_js_sdk.js on the page, if not present.
*/
loadSdk(): Promise<any>;
/**
* Checks if the user is logged in and has token and cookies in place.
*/
getLoginStatus(): Promise<any>;
/**
* Appends Yammer login button to HTML element.
*/
loginButton(selector: string): Promise<any>;
/**
* Performs Yammer rest api request.
*/
request(jQueryAjaxSettings: any): void;
/**
* Performs Yammer search.
*/
search(searchQuery: string): Promise<Array<SearchResult>>;
}

View File

@ -0,0 +1,9 @@
import { IConfiguration } from './IConfiguration';
/**
* Webpart production configuration.
*/
export class ProdConfiguration implements IConfiguration {
public readonly clientId: string = "<YOUR_YAMMER_APP_CLIENT_ID>";
public readonly redirectUri: string = "<YOUR_YAMMER_APP_REDIRECT_URI>";
}

View File

@ -0,0 +1,19 @@
/**
* Yammer search result.
*/
export class SearchResult {
public id: number;
public url: string;
public text: string;
/**
* Factory from json to result object.
*/
public static create(jsonData: any): SearchResult {
return {
id: jsonData.id,
url: jsonData.web_url,
text: jsonData.body.plain,
} as SearchResult;
}
}

View File

@ -0,0 +1,189 @@
import { IYammerProvider } from './IYammerProvider';
import { IConfiguration } from './IConfiguration';
import { SearchResult } from './SearchResult';
import { Environment, EnvironmentType } from "@microsoft/sp-core-library";
declare const window: any;
/**
* Yammer JavaScript SDK extended wrapper.
*/
export default class YammerProvider implements IYammerProvider {
private readonly _config: IConfiguration;
constructor(config: IConfiguration) {
this._config = config;
};
/**
* Appends the Yammer platform_js_sdk.js on the page, if not present.
*/
public loadSdk(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (window.hasYammerSdkLoaded) { return resolve(); }
let element: HTMLScriptElement = document.createElement("script");
element.src = "https://c64.assets-yammer.com/assets/platform_js_sdk.js";
element.async = true;
element.setAttribute("data-app-id", this._config.clientId);
document.body.appendChild(element);
window.hasYammerSdkLoaded = true;
let attempts: number = 0;
let scriptLoadedCheck: any = () => {
if (window.yam) {
resolve();
} else if (attempts === 40) { // timeout
reject();
} else {
attempts += 1;
window.setTimeout(scriptLoadedCheck, 50);
}
};
scriptLoadedCheck();
// element.onload can be used instead of setInterval. Not sure will work on all browsers.
});
};
/**
* Improved Yammer JavaScript SDK "getLoginStatus" function wrapper.
* Will attepmt to smart authenticate without prompts if the user is not logged in
* and the environment is SharePoint online.
*/
public getLoginStatus(): Promise<any> {
return new Promise((resolve, reject) => {
this._getLoginStatus()
.then((res) => {
resolve(res);
})
.catch((err) => {
if (Environment.type !== EnvironmentType.SharePoint) {
return reject(err);
}
// if SharePoint online then
// will attempt smart (no popups) authentication.
this._iframeAuthentication()
.then((res) => {
resolve(res);
})
.catch((e) => {
reject(e);
});
});
});
}
/**
* Yammer JavaScript SDK "loginButton" function wrapper.
* See https://developer.yammer.com/docs/js-sdk.
* @param selector - jQuery selector.
*/
public loginButton(selector: string): Promise<any> {
return new Promise((resolve, reject) => {
window.yam.connect.loginButton(selector, (res: any) => {
if (res.authResponse) {
resolve(res);
} else {
reject(res);
}
});
});
}
/**
* Yammer JavaScript SDK "request" function wrapper.
* Public, but not recommended for extensive use.
* More fluent methods using "request" function can be created, see the YammerProvider.search method as reference.
* See https://developer.yammer.com/docs/js-sdk.
* @param jQueryAjaxSettings - supports the jQuery.ajax() standard attributes.
*/
public request(jQueryAjaxSettings: any): void {
window.yam.platform.request(jQueryAjaxSettings);
}
/**
* Yammer search REST endpoint wrapper.
* See https://developer.yammer.com/docs/js-sdk.
* @param searchQuery
*/
public search(searchQuery: string): Promise<Array<SearchResult>> {
let results: Array<SearchResult> = new Array<SearchResult>();
return new Promise((resolve, reject) => {
this.request({
url: `search.json?search=${window.encodeURIComponent(searchQuery)}`,
success: (res) => {
if (res.messages) {
for (let i: number = 0; i < res.messages.messages.length; i++) {
results.push(SearchResult.create(res.messages.messages[i]));
}
}
resolve(results);
},
error: (err) => {
reject(err);
}
});
});
};
/**
* Yammer JavaScript SDK "getLoginStatus" function wrapper.
* See https://developer.yammer.com/docs/js-sdk.
*/
private _getLoginStatus(): Promise<any> {
return new Promise((resolve, reject) => {
window.yam.getLoginStatus((res: any) => {
if (res.authResponse) {
resolve(res);
} else {
reject(res);
}
});
});
};
/**
* Authenticating Office 365 tenant user with OAuth2 call in iframe.
* Technique used by Microsoft for the Yammer embed OOTB modern webpart,
* discovered by Joseph King (@7kingjoe3).
* Make sure the client Id and redirect Uri are as specified in the registered Yammer app.
*/
private _iframeAuthentication(): Promise<any> {
let self: YammerProvider = this;
return new Promise((resolve, reject) => {
let iframeId: string = "authIframe";
let element: HTMLIFrameElement = document.createElement("iframe");
element.setAttribute("id", iframeId);
element.setAttribute("style", "display:none");
document.body.appendChild(element);
element.addEventListener("load", _ => {
try {
let elem: HTMLIFrameElement = document.getElementById(iframeId) as HTMLIFrameElement;
let token: string = elem.contentWindow.location.hash.split("=")[1];
window.yam.platform.setAuthToken(token);
this._getLoginStatus()
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
} catch (ex) {
reject(ex);
}
});
let domainName: string = window.location.host.split(".")[0];
let queryString: string = `client_id=${self._config.clientId}&response_type=token&redirect_uri=${self._config.redirectUri}`;
let url: string = `https://www.yammer.com/${domainName}.onmicrosoft.com/oauth2/authorize?${queryString}`;
element.src = url;
// timeout reject promise can be added here, but this is too much defensive programing for me.
});
}
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,8 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />