diff --git a/samples/react-officegraph/README.md b/samples/react-officegraph/README.md index 6357219cf..5566ef541 100644 --- a/samples/react-officegraph/README.md +++ b/samples/react-officegraph/README.md @@ -22,6 +22,12 @@ Sample SharePoint Framework Client-Side Web Part built using React showing docum ![Working with Web Part in the SharePoint Workbench](./assets/my-recent-documents-preview.png) +### Trending in the sites I follow + +Sample SharePoint Framework Client-Side Web Part built using React showing documents trending in the sites followed by the current user. + +![Working with Web Part in the SharePoint Workbench](./assets/trending-in-sites-i-follow-preview.png) + ## Applies to * [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview) @@ -37,6 +43,7 @@ react-officegraph|Waldek Mastykarz (MVP, Rencore, @waldekm) Version|Date|Comments -------|----|-------- +1.3.0|September 20, 2016|Added the Trending in the sites I follow sample 1.2.0|September 20, 2016|Added the My recent documents sample 1.1.0|September 19, 2016|Added the Working with sample 1.0.0|September 9, 2016|Initial release diff --git a/samples/react-officegraph/assets/trending-in-sites-i-follow-preview.png b/samples/react-officegraph/assets/trending-in-sites-i-follow-preview.png new file mode 100644 index 000000000..0c0b7431a Binary files /dev/null and b/samples/react-officegraph/assets/trending-in-sites-i-follow-preview.png differ diff --git a/samples/react-officegraph/config/config.json b/samples/react-officegraph/config/config.json index b7d27e294..a4499e1f8 100644 --- a/samples/react-officegraph/config/config.json +++ b/samples/react-officegraph/config/config.json @@ -14,6 +14,11 @@ "entry": "./lib/webparts/myRecentDocuments/MyRecentDocumentsWebPart.js", "manifest": "./src/webparts/myRecentDocuments/MyRecentDocumentsWebPart.manifest.json", "outputPath": "./dist/my-recent-documents.bundle.js" + }, + { + "entry": "./lib/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollowWebPart.js", + "manifest": "./src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollowWebPart.manifest.json", + "outputPath": "./dist/trending-in-the-sites-i-follow.bundle.js" } ], "externals": { @@ -28,6 +33,7 @@ "localizedResources": { "trendingInThisSiteStrings": "webparts/trendingInThisSite/loc/{locale}.js", "workingWithStrings": "webparts/workingWith/loc/{locale}.js", - "myRecentDocumentsStrings": "webparts/myRecentDocuments/loc/{locale}.js" + "myRecentDocumentsStrings": "webparts/myRecentDocuments/loc/{locale}.js", + "trendingInTheSitesIFollowStrings": "webparts/trendingInTheSitesIFollow/loc/{locale}.js" } } diff --git a/samples/react-officegraph/package.json b/samples/react-officegraph/package.json index 2b826d407..afaae775e 100644 --- a/samples/react-officegraph/package.json +++ b/samples/react-officegraph/package.json @@ -1,6 +1,6 @@ { "name": "react-officegraph", - "version": "1.2.0", + "version": "1.3.0", "private": true, "engines": { "node": ">=0.10.0" diff --git a/samples/react-officegraph/src/webparts/ITrendingDocument.ts b/samples/react-officegraph/src/webparts/ITrendingDocument.ts index ec68d685d..00c688201 100644 --- a/samples/react-officegraph/src/webparts/ITrendingDocument.ts +++ b/samples/react-officegraph/src/webparts/ITrendingDocument.ts @@ -4,6 +4,8 @@ export interface ITrendingDocument { id: string; title: string; url: string; + webUrl?: string; + webTitle?: string; previewImageUrl: string; extension: string; activity: IActivity; diff --git a/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/ITrendingInTheSitesIFollowWebPartProps.ts b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/ITrendingInTheSitesIFollowWebPartProps.ts new file mode 100644 index 000000000..8dcab38ab --- /dev/null +++ b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/ITrendingInTheSitesIFollowWebPartProps.ts @@ -0,0 +1,4 @@ +export interface ITrendingInTheSitesIFollowWebPartProps { + title: string; + numberOfDocuments: number; +} diff --git a/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollow.module.scss b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollow.module.scss new file mode 100644 index 000000000..a980b2c39 --- /dev/null +++ b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollow.module.scss @@ -0,0 +1,16 @@ +.trendingInTheSitesIFollow { + .webPartTitle { + margin-bottom: 0.7em; + margin-left: 0.38em; + } + + :global .ms-DocumentCard { + float: left; + margin: 0.5em; + } + + :global .ms-Spinner { + width: 7em; + margin: 0 auto; + } +} diff --git a/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollowWebPart.manifest.json b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollowWebPart.manifest.json new file mode 100644 index 000000000..f1dc2850c --- /dev/null +++ b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollowWebPart.manifest.json @@ -0,0 +1,20 @@ +{ + "$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json", + + "id": "90a04bb9-fbdf-43d1-bfe1-0491d34941be", + "componentType": "WebPart", + "version": "0.0.1", + "manifestVersion": 2, + + "preconfiguredEntries": [{ + "groupId": "90a04bb9-fbdf-43d1-bfe1-0491d34941be", + "group": { "default": "Content rollup" }, + "title": { "default": "Trending in the sites I follow" }, + "description": { "default": "Shows documents trending in the sites followed by the current user" }, + "officeFabricIconFontName": "Chart", + "properties": { + "title": "Trending in the sites I follow", + "numberOfDocuments": 5 + } + }] +} diff --git a/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollowWebPart.ts b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollowWebPart.ts new file mode 100644 index 000000000..6c7af2a11 --- /dev/null +++ b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/TrendingInTheSitesIFollowWebPart.ts @@ -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 'trendingInTheSitesIFollowStrings'; +import TrendingInTheSitesIFollow, { ITrendingInTheSitesIFollowProps } from './components/TrendingInTheSitesIFollow'; +import { ITrendingInTheSitesIFollowWebPartProps } from './ITrendingInTheSitesIFollowWebPartProps'; + +export default class TrendingInTheSitesIFollowWebPart extends BaseClientSideWebPart { + + public constructor(context: IWebPartContext) { + super(context); + } + + public render(): void { + const element: React.ReactElement = React.createElement(TrendingInTheSitesIFollow, { + title: this.properties.title, + numberOfDocuments: this.properties.numberOfDocuments, + 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('numberOfDocuments', { + label: strings.NumberOfDocumentsFieldLabel, + min: 1, + max: 10, + step: 1 + }) + ] + } + ] + } + ] + }; + } +} diff --git a/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/components/TrendingInTheSitesIFollow.tsx b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/components/TrendingInTheSitesIFollow.tsx new file mode 100644 index 000000000..50e0e5839 --- /dev/null +++ b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/components/TrendingInTheSitesIFollow.tsx @@ -0,0 +1,252 @@ +import * as React from 'react'; +import { + css, + DocumentCard, + DocumentCardLocation, + DocumentCardPreview, + DocumentCardTitle, + DocumentCardActivity, + Spinner +} from 'office-ui-fabric-react'; + +import styles from '../TrendingInTheSitesIFollow.module.scss'; +import { ITrendingInTheSitesIFollowWebPartProps } from '../ITrendingInTheSitesIFollowWebPartProps'; +import { HttpClient } from '@microsoft/sp-client-base'; +import { ITrendingDocument } from '../../ITrendingDocument'; +import { IActorInformation } from '../../IActorInformation'; +import { SearchUtils, ISearchQueryResponse, IRow, ICell, IEdge } from '../../SearchUtils'; +import { Utils } from '../../Utils'; + +export interface ITrendingInTheSitesIFollowProps extends ITrendingInTheSitesIFollowWebPartProps { + httpClient: HttpClient; + siteUrl: string; +} + +export interface ITrendingInTheSitesIFollowState { + trendingDocuments: ITrendingDocument[]; + loading: boolean; + error: string; +} + +interface ISiteInfo { + Uri: string; +} + +export default class TrendingInTheSitesIFollow extends React.Component { + constructor(props: ITrendingInTheSitesIFollowProps, state: ITrendingInTheSitesIFollowState) { + super(props); + + this.state = { + trendingDocuments: [] as ITrendingDocument[], + loading: true, + error: null + }; + } + + public componentDidMount(): void { + this.loadDocuments(this.props.siteUrl, this.props.numberOfDocuments); + } + + public componentDidUpdate(prevProps: ITrendingInTheSitesIFollowProps, prevState: ITrendingInTheSitesIFollowState, prevContext: any): void { + if (this.props.numberOfDocuments !== prevProps.numberOfDocuments || + this.props.siteUrl !== prevProps.siteUrl && ( + this.props.numberOfDocuments && this.props.siteUrl + )) { + this.loadDocuments(this.props.siteUrl, this.props.numberOfDocuments); + } + } + + public render(): JSX.Element { + const loading: JSX.Element = this.state.loading ?
:
; + const error: JSX.Element = this.state.error ?
Error: {this.state.error}
:
; + const documents: JSX.Element[] = this.state.trendingDocuments.map((doc: ITrendingDocument, i: number) => { + const iconUrl: string = `https://spoprod-a.akamaihd.net/files/odsp-next-prod_ship-2016-08-15_20160815.002/odsp-media/images/filetypes/32/${doc.extension}.png`; + return ( + + + + + + + ); + }); + return ( +
+
{this.props.title}
+ {loading} + {error} + {documents} +
+
+ ); + } + + private loadDocuments(siteUrl: string, numberOfDocuments: number): void { + this.setState({ + loading: true, + error: undefined, + trendingDocuments: [] + }); + const trendingDocuments: ITrendingDocument[] = []; + this.getSitesIFollow(siteUrl) + .then((sitesIFollow: string[]): Promise => { + return this.getTrendingDocuments(sitesIFollow, siteUrl, numberOfDocuments); + }) + .then((trendingDocuments: ITrendingDocument[]): void => { + this.setState({ + loading: false, + error: undefined, + trendingDocuments: trendingDocuments + }); + }, (error: any): void => { + this.setState({ + loading: false, + error: error, + trendingDocuments: [] + }); + }); + + return; + } + + private getSitesIFollow(siteUrl: string): Promise { + return new Promise((resolve: (sitesIFollow: string[]) => void, reject: (error: any) => void): void => { + this.props.httpClient.get(`${siteUrl}/_api/social.following/my/followed(types=4)`, { + headers: { + 'Accept': 'application/json;odata=nometadata', + 'odata-version': '' + } + }) + .then((response: Response): Promise<{ value: ISiteInfo[] }> => { + return response.json(); + }) + .then((sitesIFollowInfo: { value: ISiteInfo[] }): void => { + const sitesIFollow: string[] = []; + + for (let i: number = 0; i < sitesIFollowInfo.value.length; i++) { + sitesIFollow.push(sitesIFollowInfo.value[i].Uri); + } + + resolve(sitesIFollow); + }, (error: any): void => { + reject(error); + }); + }); + } + + private getTrendingDocuments(sitesIFollow: string[], siteUrl: string, numberOfDocuments: number): Promise { + return new Promise((resolve: (trendingDocuments: ITrendingDocument[]) => void, reject: (error: any) => void): void => { + if (sitesIFollow.length === 0) { + return resolve([]); + } + + let query: string = '('; + for (let i: number = 0; i < sitesIFollow.length; i++) { + if (query.length > 1) { + query += ' OR '; + } + + query += `Path:"${sitesIFollow[i]}"`; + } + query += ') AND (IsDocument:1)'; + + const postData: string = JSON.stringify({ + 'request': { + '__metadata': { + 'type': 'Microsoft.Office.Server.Search.REST.SearchRequest' + }, + 'Querytext': query, + 'SelectProperties': { + 'results': ['Author', 'AuthorOwsUser', 'DocId', 'DocumentPreviewMetadata', 'Edges', 'EditorOwsUser', 'FileExtension', 'FileType', 'HitHighlightedProperties', 'HitHighlightedSummary', 'LastModifiedTime', 'LikeCountLifetime', 'ListID', 'ListItemID', 'OriginalPath', 'Path', 'Rank', 'SPWebUrl', 'SecondaryFileExtension', 'ServerRedirectedURL', 'SiteTitle', 'Title', 'ViewCountLifetime', 'siteID', 'uniqueID', 'webID'] + }, + 'ClientType': 'TrendingInTheSitesIFollow', + 'BypassResultTypes': 'true', + 'RowLimit': numberOfDocuments, + 'StartRow': '0', + 'RankingModelId': '0c77ded8-c3ef-466d-929d-905670ea1d72', + 'Properties': { + 'results': [{ + 'Name': 'IncludeExternalContent', + 'Value': { + 'BoolVal': 'True', + 'QueryPropertyValueTypeIndex': 3 + } + }, { + 'Name': 'GraphQuery', + 'Value': { + 'StrVal': 'actor(ME,action:1021)', + 'QueryPropertyValueTypeIndex': 1 + } + }] + } + } + }); + this.props.httpClient.post(`${siteUrl}/_api/search/postquery`, { + headers: { + 'Accept': 'application/json;odata=nometadata', + 'Content-type': 'application/json;odata=verbose', + 'odata-version': '' + }, + body: postData + }) + .then((response: Response): Promise => { + return response.json(); + }) + .then((response: ISearchQueryResponse): void => { + if (!response || + !response.PrimaryQueryResult || + !response.PrimaryQueryResult.RelevantResults || + response.PrimaryQueryResult.RelevantResults.RowCount === 0) { + resolve([]); + return; + } + + const trendingDocuments: ITrendingDocument[] = []; + for (let i: number = 0; i < response.PrimaryQueryResult.RelevantResults.Table.Rows.length; i++) { + const row: IRow = response.PrimaryQueryResult.RelevantResults.Table.Rows[i]; + const cells: ICell[] = row.Cells; + const editorInfo: string[] = SearchUtils.getValueFromResults('EditorOwsUser', cells).split('|'); + const modifiedDate: Date = new Date(SearchUtils.getValueFromResults('LastModifiedTime', cells).replace('.0000000', '')); + const dateString: string = (modifiedDate.getMonth() + 1) + '/' + modifiedDate.getDate() + '/' + modifiedDate.getFullYear(); + trendingDocuments.push({ + id: SearchUtils.getValueFromResults('DocId', cells), + url: SearchUtils.getValueFromResults('ServerRedirectedURL', cells), + webUrl: SearchUtils.getValueFromResults('SPWebUrl', cells), + webTitle: SearchUtils.getValueFromResults('SiteTitle', cells), + title: SearchUtils.getValueFromResults('Title', cells), + previewImageUrl: SearchUtils.getPreviewImageUrl(cells, siteUrl), + extension: SearchUtils.getValueFromResults('FileType', cells), + activity: { + actorId: -1, + actorName: Utils.trim(editorInfo[1]), + actorPhotoUrl: Utils.getUserPhotoUrl(Utils.trim(editorInfo[0]), siteUrl), + date: dateString, + name: 'Modified' + } + }); + } + + resolve(trendingDocuments); + }, (error: any): void => { + reject(error); + }); + }); + } +} diff --git a/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/loc/en-us.js b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/loc/en-us.js new file mode 100644 index 000000000..53d829350 --- /dev/null +++ b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/loc/en-us.js @@ -0,0 +1,8 @@ +define([], function() { + return { + "PropertyPaneDescription": "Manage the settings of this Web Part", + "ViewGroupName": "View", + "NumberOfDocumentsFieldLabel": "Number of documents to show", + "TitleFieldLabel": "Web Part Title (displayed in the body)" + } +}); \ No newline at end of file diff --git a/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/loc/mystrings.d.ts b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/loc/mystrings.d.ts new file mode 100644 index 000000000..52af4688a --- /dev/null +++ b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/loc/mystrings.d.ts @@ -0,0 +1,11 @@ +declare interface ITrendingInTheSitesIFollowStrings { + PropertyPaneDescription: string; + ViewGroupName: string; + NumberOfDocumentsFieldLabel: string; + TitleFieldLabel: string; +} + +declare module 'trendingInTheSitesIFollowStrings' { + const strings: ITrendingInTheSitesIFollowStrings; + export = strings; +} diff --git a/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/tests/TrendingInTheSitesIFollow.test.ts b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/tests/TrendingInTheSitesIFollow.test.ts new file mode 100644 index 000000000..68facb2e7 --- /dev/null +++ b/samples/react-officegraph/src/webparts/trendingInTheSitesIFollow/tests/TrendingInTheSitesIFollow.test.ts @@ -0,0 +1,7 @@ +import * as assert from 'assert'; + +describe('TrendingInTheSitesIFollowWebPart', () => { + it('should do something', () => { + assert.ok(true); + }); +});