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)
|
||||
|
||||
### 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 |
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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