Added the 'Working with' sample (#20)

This commit is contained in:
Waldek Mastykarz 2016-09-19 15:51:03 +02:00 committed by GitHub
parent 557241385f
commit e46f3b5606
14 changed files with 346 additions and 3 deletions

View File

@ -10,6 +10,12 @@ Sample SharePoint Framework Client-Side Web Part built using React showing docum
![Trending in this site Web Part in the SharePoint Workbench](./assets/trendinginthissite-preview.png)
### Working with
Sample SharePoint Framework Client-Side Web Part built using React showing people with whom the current user has recently been working with.
![Working with Web Part in the SharePoint Workbench](./assets/working-with-preview.png)
## Applies to
* [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
@ -25,7 +31,8 @@ react-officegraph|Waldek Mastykarz (MVP, Rencore, @waldekm)
Version|Date|Comments
-------|----|--------
1.0|September 9, 2016|Initial release
1.1.0|September 19, 2016|Added the Working with sample
1.0.0|September 9, 2016|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.**
@ -62,4 +69,17 @@ This Web Part illustrates the following concepts on top of the SharePoint Framew
- passing Web Part properties to React components
- using ES6 Promises with vanilla-JavaScript web requests
### Working with Web Part
The _Working with_ Client-Side Web Part is built on the SharePoint Framework using React and uses the [Office UI Fabric React](https://github.com/OfficeDev/office-ui-fabric-react) to show persona cards.
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 components for building user experience consistent with SharePoint and Office
- communicating with SharePoint using its REST API
- communicating with the Office Graph via the SharePoint Search REST API
- passing Web Part properties to React components
- using ES6 Promises with vanilla-JavaScript web requests
![](https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-officegraph)

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -4,6 +4,11 @@
"entry": "./lib/webparts/trendingInThisSite/TrendingInThisSiteWebPart.js",
"manifest": "./src/webparts/trendingInThisSite/TrendingInThisSiteWebPart.manifest.json",
"outputPath": "./dist/trending-in-this-site.bundle.js"
},
{
"entry": "./lib/webparts/workingWith/WorkingWithWebPart.js",
"manifest": "./src/webparts/workingWith/WorkingWithWebPart.manifest.json",
"outputPath": "./dist/working-with.bundle.js"
}
],
"externals": {
@ -16,6 +21,7 @@
"react-dom/server": "node_modules/react-dom/dist/react-dom-server.min.js"
},
"localizedResources": {
"trendingInThisSiteStrings": "webparts/trendingInThisSite/loc/{locale}.js"
"trendingInThisSiteStrings": "webparts/trendingInThisSite/loc/{locale}.js",
"workingWithStrings": "webparts/workingWith/loc/{locale}.js"
}
}

View File

@ -1,10 +1,14 @@
{
"name": "react-officegraph",
"version": "0.0.1",
"version": "1.1.0",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"author": {
"name": "Waldek Mastykarz",
"url": "http://www.rencore.com"
},
"dependencies": {
"@microsoft/sp-client-base": "~0.2.0",
"@microsoft/sp-client-preview": "~0.2.0",

View File

@ -0,0 +1,44 @@
export interface ISearchQueryResponse {
PrimaryQueryResult: IPrimaryQueryResult;
}
export interface IPrimaryQueryResult {
RelevantResults?: IRelevantResults;
}
export interface IRelevantResults {
RowCount: number;
Table?: ITable;
}
export interface ITable {
Rows?: IRow[];
}
export interface IRow {
Cells: ICell[];
}
export interface ICell {
Key: string;
Value: string;
ValueType: string;
}
export class SearchUtils {
public static getValueFromResults(key: string, results: ICell[]): string {
let value: string = '';
if (results && results.length > 0 && key) {
for (let i: number = 0; i < results.length; i++) {
const resultItem: ICell = results[i];
if (resultItem.Key === key) {
value = resultItem.Value;
break;
}
}
}
return value;
}
}

View File

@ -0,0 +1,5 @@
export class Utils {
public static getUserPhotoUrl(userEmail: string, siteUrl: string, size: string = 'S'): string {
return `${siteUrl}/_layouts/15/userphoto.aspx?size=${size}&accountname=${userEmail}`;
}
}

View File

@ -0,0 +1,4 @@
export interface IWorkingWithWebPartProps {
numberOfPeople: number;
title: string;
}

View File

@ -0,0 +1,14 @@
.workingWith {
.webPartTitle {
margin-bottom: 1em;
}
:global .ms-Spinner {
width: 7em;
margin: 0 auto;
}
:global .ms-Persona {
cursor: pointer;
}
}

View File

@ -0,0 +1,20 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "79b943ce-52dd-47ca-aeee-e46b277260de",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [{
"groupId": "79b943ce-52dd-47ca-aeee-e46b277260de",
"group": { "default": "Productivity" },
"title": { "default": "Working with" },
"description": { "default": "Shows people with whom you communicate frequently" },
"officeFabricIconFontName": "Group",
"properties": {
"title": "Recent contacts",
"numberOfPeople": 5
}
}]
}

View File

@ -0,0 +1,59 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import {
BaseClientSideWebPart,
IPropertyPaneSettings,
IWebPartContext,
PropertyPaneTextField,
PropertyPaneSlider
} from '@microsoft/sp-client-preview';
import * as strings from 'workingWithStrings';
import WorkingWith, { IWorkingWithProps } from './components/WorkingWith';
import { IWorkingWithWebPartProps } from './IWorkingWithWebPartProps';
export default class WorkingWithWebPart extends BaseClientSideWebPart<IWorkingWithWebPartProps> {
public constructor(context: IWebPartContext) {
super(context);
}
public render(): void {
const element: React.ReactElement<IWorkingWithProps> = React.createElement(WorkingWith, {
numberOfPeople: this.properties.numberOfPeople,
title: this.properties.title,
httpClient: this.context.httpClient,
siteUrl: this.context.pageContext.web.absoluteUrl
});
ReactDom.render(element, this.domElement);
}
protected get propertyPaneSettings(): IPropertyPaneSettings {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.ViewGroupName,
groupFields: [
PropertyPaneTextField('title', {
label: strings.TitleFieldLabel
}),
PropertyPaneSlider('numberOfPeople', {
label: strings.NumberOfPeopleFieldLabel,
min: 1,
max: 10,
step: 1
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,141 @@
import * as React from 'react';
import styles from '../WorkingWith.module.scss';
import { IWorkingWithWebPartProps } from '../IWorkingWithWebPartProps';
import { HttpClient } from '@microsoft/sp-client-base';
import { SearchUtils, ISearchQueryResponse, IRow } from '../../SearchUtils';
import { Utils } from '../../Utils';
import {
css,
Persona,
PersonaSize,
PersonaPresence,
Spinner
} from 'office-ui-fabric-react';
export interface IWorkingWithProps extends IWorkingWithWebPartProps {
httpClient: HttpClient;
siteUrl: string;
}
export interface IPerson {
name: string;
email: string;
jobTitle: string;
department: string;
photoUrl: string;
profileUrl: string;
}
export interface IWorkingWithState {
loading: boolean;
people: IPerson[];
error: string;
}
export default class WorkingWith extends React.Component<IWorkingWithProps, IWorkingWithState> {
constructor(props: IWorkingWithProps, state: IWorkingWithState) {
super(props);
this.state = {
people: [],
loading: true,
error: null
};
}
public componentDidMount(): void {
this.loadPeople(this.props.siteUrl, this.props.numberOfPeople);
}
public componentDidUpdate(prevProps: IWorkingWithProps, prevState: IWorkingWithState, prevContext: any): void {
if (this.props.numberOfPeople !== prevProps.numberOfPeople ||
this.props.siteUrl !== prevProps.siteUrl && (
this.props.numberOfPeople && this.props.siteUrl
)) {
this.loadPeople(this.props.siteUrl, this.props.numberOfPeople);
}
}
public render(): JSX.Element {
const loading: JSX.Element = this.state.loading ? <div style={{ margin: '0 auto' }}><Spinner label={'Loading...'} /></div> : <div/>;
const error: JSX.Element = this.state.error ? <div><strong>Error: </strong> {this.state.error}</div> : <div/>;
const people: JSX.Element[] = this.state.people.map((person: IPerson, i: number) => {
return (
<Persona
primaryText={person.name}
secondaryText={person.jobTitle}
tertiaryText={person.department}
imageUrl={person.photoUrl}
size={PersonaSize.large}
presence={PersonaPresence.none}
onClick={() => { this.navigateTo(person.profileUrl); } }
key={person.email} />
);
});
return (
<div className={styles.workingWith}>
<div className={css('ms-font-xl', styles.webPartTitle)}>{this.props.title}</div>
{loading}
{error}
{people}
</div>
);
}
private navigateTo(url: string): void {
window.open(url, '_blank');
}
private loadPeople(siteUrl: string, numberOfPeople: number): void {
this.props.httpClient.get(`${siteUrl}/_api/search/query?querytext='*'&properties='GraphQuery:actor(me\\,action\\:1019)'&selectproperties='Title,WorkEmail,JobTitle,Department,Path'&rowlimit=${numberOfPeople}`, {
headers: {
'Accept': 'application/json;odata=nometadata',
'odata-version': ''
}
})
.then((response: Response): Promise<ISearchQueryResponse> => {
return response.json();
})
.then((response: ISearchQueryResponse): void => {
if (!response ||
!response.PrimaryQueryResult ||
!response.PrimaryQueryResult.RelevantResults ||
response.PrimaryQueryResult.RelevantResults.RowCount === 0) {
this.setState({
loading: false,
error: null,
people: []
});
return;
}
const people: IPerson[] = [];
for (let i: number = 0; i < response.PrimaryQueryResult.RelevantResults.Table.Rows.length; i++) {
const personRow: IRow = response.PrimaryQueryResult.RelevantResults.Table.Rows[i];
const email: string = SearchUtils.getValueFromResults('WorkEmail', personRow.Cells);
people.push({
name: SearchUtils.getValueFromResults('Title', personRow.Cells),
email: email,
jobTitle: SearchUtils.getValueFromResults('JobTitle', personRow.Cells),
department: SearchUtils.getValueFromResults('Department', personRow.Cells),
photoUrl: Utils.getUserPhotoUrl(email, siteUrl, 'L'),
profileUrl: SearchUtils.getValueFromResults('Path', personRow.Cells)
});
}
this.setState({
loading: false,
error: null,
people: people
});
}, (error: any): void => {
this.setState({
loading: false,
error: error,
people: []
});
});
}
}

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"PropertyPaneDescription": "Manage the settings of this Web Part",
"ViewGroupName": "View",
"NumberOfPeopleFieldLabel": "Number of people to show",
"TitleFieldLabel": "Web Part Title (displayed in the body)"
}
});

View File

@ -0,0 +1,11 @@
declare interface IWorkingWithStrings {
PropertyPaneDescription: string;
ViewGroupName: string;
NumberOfPeopleFieldLabel: string;
TitleFieldLabel: string;
}
declare module 'workingWithStrings' {
const strings: IWorkingWithStrings;
export = strings;
}

View File

@ -0,0 +1,7 @@
import * as assert from 'assert';
describe('WorkingWithWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});