Added the 'Working with' sample (#20)
This commit is contained in:
parent
557241385f
commit
e46f3b5606
|
@ -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)
|
![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
|
## Applies to
|
||||||
|
|
||||||
* [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
|
* [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
|
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
|
## 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.**
|
**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
|
- passing Web Part properties to React components
|
||||||
- using ES6 Promises with vanilla-JavaScript web requests
|
- 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)
|
![](https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-officegraph)
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
|
@ -4,6 +4,11 @@
|
||||||
"entry": "./lib/webparts/trendingInThisSite/TrendingInThisSiteWebPart.js",
|
"entry": "./lib/webparts/trendingInThisSite/TrendingInThisSiteWebPart.js",
|
||||||
"manifest": "./src/webparts/trendingInThisSite/TrendingInThisSiteWebPart.manifest.json",
|
"manifest": "./src/webparts/trendingInThisSite/TrendingInThisSiteWebPart.manifest.json",
|
||||||
"outputPath": "./dist/trending-in-this-site.bundle.js"
|
"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": {
|
"externals": {
|
||||||
|
@ -16,6 +21,7 @@
|
||||||
"react-dom/server": "node_modules/react-dom/dist/react-dom-server.min.js"
|
"react-dom/server": "node_modules/react-dom/dist/react-dom-server.min.js"
|
||||||
},
|
},
|
||||||
"localizedResources": {
|
"localizedResources": {
|
||||||
"trendingInThisSiteStrings": "webparts/trendingInThisSite/loc/{locale}.js"
|
"trendingInThisSiteStrings": "webparts/trendingInThisSite/loc/{locale}.js",
|
||||||
|
"workingWithStrings": "webparts/workingWith/loc/{locale}.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "react-officegraph",
|
"name": "react-officegraph",
|
||||||
"version": "0.0.1",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
},
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "Waldek Mastykarz",
|
||||||
|
"url": "http://www.rencore.com"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/sp-client-base": "~0.2.0",
|
"@microsoft/sp-client-base": "~0.2.0",
|
||||||
"@microsoft/sp-client-preview": "~0.2.0",
|
"@microsoft/sp-client-preview": "~0.2.0",
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface IWorkingWithWebPartProps {
|
||||||
|
numberOfPeople: number;
|
||||||
|
title: string;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
.workingWith {
|
||||||
|
.webPartTitle {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .ms-Spinner {
|
||||||
|
width: 7em;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .ms-Persona {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
|
@ -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
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
declare interface IWorkingWithStrings {
|
||||||
|
PropertyPaneDescription: string;
|
||||||
|
ViewGroupName: string;
|
||||||
|
NumberOfPeopleFieldLabel: string;
|
||||||
|
TitleFieldLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'workingWithStrings' {
|
||||||
|
const strings: IWorkingWithStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
describe('WorkingWithWebPart', () => {
|
||||||
|
it('should do something', () => {
|
||||||
|
assert.ok(true);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue