From a0c73393c804bf9c3f202a0a61096b4503d0eb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lage?= Date: Thu, 25 Nov 2021 18:17:13 +0100 Subject: [PATCH 1/7] updated to use Microsoft Graph follow --- samples/react-follow-document/README.md | 13 +- .../config/package-solution.json | 53 ++- .../followDocumentWebPart/Service/Rest.ts | 146 ------ .../components/FollowDocumentWebPart.tsx | 433 ++++++++++-------- .../components/IFollowDocumentWebPartState.ts | 3 +- .../followDocumentDialog.tsx | 9 +- .../IfollowDocumentPreviewProps.ts | 2 + .../followDocumentPreview.tsx | 15 +- .../IfollowDocumentSendMessageProps.ts | 3 +- .../followDocumentSendMessage.tsx | 9 +- .../models/followDocument.ts | 23 + 11 files changed, 331 insertions(+), 378 deletions(-) delete mode 100644 samples/react-follow-document/src/webparts/followDocumentWebPart/Service/Rest.ts create mode 100644 samples/react-follow-document/src/webparts/followDocumentWebPart/models/followDocument.ts diff --git a/samples/react-follow-document/README.md b/samples/react-follow-document/README.md index b2fd71118..a15f3bf79 100644 --- a/samples/react-follow-document/README.md +++ b/samples/react-follow-document/README.md @@ -2,7 +2,7 @@ ## Summary -This solution has the goal to easily identify/follow user key documents from all Tenant and easily access them in Modern Pages and Microsoft Teams. This solution uses the Out of Box Social feature **"Follow Document WebPart"** with combination of MSGraph queries and extension for Microsoft Teams. +This solution has the goal to easily identify/follow user key documents from all Tenant and easily access them in Modern Pages and Microsoft Teams. This solution uses the Out of Box Office Page > Favorites Files Tab with combination of MSGraph queries and extension for Microsoft Teams. This is a 2 phase project with associated dependency of solution [Follow-Document](https://github.com/pnp/sp-dev-fx-extensions/tree/main/samples/react-command-follow-document) extension where users are allow to select and manage Followed Document in Libraries to be used in this project. @@ -20,8 +20,9 @@ Available features: - Microsoft Team integration with personal/Tab App that allow user focus on key Documents. Usage of following Technologies: -- Usage of Social Feature **"Follow" documents** and associated REST "[/_api/social.following/](https://docs.microsoft.com/en-us/sharepoint/dev/general-development/how-to-follow-documents-sites-and-tags-by-using-the-rest-service-in-sharepoint-2)" -- Usage of Graph queries using [Graph explorer](https://developer.microsoft.com/en-us/graph/graph-explorer) +- Usage of Microsoft Graph API "[Follow drive item](https://docs.microsoft.com/en-us/graph/api/driveitem-follow?view=graph-rest-1.0&tabs=http)" +- Usage of Microsoft Graph API "[Unfollow drive item](https://docs.microsoft.com/en-us/graph/api/driveitem-unfollow?view=graph-rest-1.0&tabs=http)" +- Usage of Microsoft Graph API "[List followed items](https://docs.microsoft.com/en-us/graph/api/drive-list-following?view=graph-rest-1.0&tabs=http)"- Usage of Graph queries using [Graph explorer](https://developer.microsoft.com/en-us/graph/graph-explorer) - Usage of [adaptive cards](https://adaptivecards.io/) - Microsoft Teams integration with following option [TeamsTab, TeamsPersonalApp] @@ -57,6 +58,8 @@ o365 spo login https://contoso-admin.sharepoint.com o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Files.Read' o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Files.Read.All' o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Sites.Read.All' +o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Files.ReadWrite.All' +o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Sites.ReadWrite.All' o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Team.ReadBasic.All' o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Channel.ReadBasic.All' o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'ChannelMessage.Send' @@ -73,7 +76,7 @@ react-follow-document | [André Lage](https://github.com/aaclage) (http://aaclag Version|Date|Comments -------|----|-------- 1.0|June 22, 2021|Initial release - +2.0|November 25, 2021|Change to use Microsoft Graph Follow ## Minimal Path to Awesome @@ -93,7 +96,7 @@ Description of the extension that expands upon high-level summary above. This extension illustrates the following concepts: -- Change of SharePoint Social Feature **"Follow"** to follow key Documents for users in Modern Sites. +- Usage of Office > Favorites to follow key Documents from users in Modern Sites. - Simple UX to manage **Followed** documents and report list followed documents across Tenant and access properties and Preview of Document. - Option to unfollow documents individually. - Integration with other services of Office 365 such us (Preview, Microsoft Team Messages). diff --git a/samples/react-follow-document/config/package-solution.json b/samples/react-follow-document/config/package-solution.json index 066dba076..85d6c1636 100644 --- a/samples/react-follow-document/config/package-solution.json +++ b/samples/react-follow-document/config/package-solution.json @@ -14,25 +14,40 @@ "termsOfUseUrl": "", "mpnId": "" }, - "webApiPermissionRequests": [{ - "resource": "Microsoft Graph", - "scope": "Files.Read" - }, { - "resource": "Microsoft Graph", - "scope": "Files.Read.All" - }, { - "resource": "Microsoft Graph", - "scope": "Sites.Read.All" - }, { - "resource": "Microsoft Graph", - "scope": "Team.ReadBasic.All" - }, { - "resource": "Microsoft Graph", - "scope": "Channel.ReadBasic.All" - }, { - "resource": "Microsoft Graph", - "scope": "ChannelMessage.Send" - }] + "webApiPermissionRequests": [ + { + "resource": "Microsoft Graph", + "scope": "Files.Read" + }, + { + "resource": "Microsoft Graph", + "scope": "Files.ReadWrite.All" + }, + { + "resource": "Microsoft Graph", + "scope": "Sites.ReadWrite.All" + }, + { + "resource": "Microsoft Graph", + "scope": "Files.Read.All" + }, + { + "resource": "Microsoft Graph", + "scope": "Sites.Read.All" + }, + { + "resource": "Microsoft Graph", + "scope": "Team.ReadBasic.All" + }, + { + "resource": "Microsoft Graph", + "scope": "Channel.ReadBasic.All" + }, + { + "resource": "Microsoft Graph", + "scope": "ChannelMessage.Send" + } + ] }, "paths": { "zippedPackage": "solution/follow-document-web-part.sppkg" diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/Service/Rest.ts b/samples/react-follow-document/src/webparts/followDocumentWebPart/Service/Rest.ts deleted file mode 100644 index 582e77b5d..000000000 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/Service/Rest.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { - SPHttpClient, - SPHttpClientResponse, - ISPHttpClientOptions, - } from "@microsoft/sp-http"; - import * as MicrosoftGraph from "@microsoft/microsoft-graph-types"; - -export default class Rest { - public async isfollowed( - spHttpClient: SPHttpClient, - fileUrl: string, - siteUrl: string - ): Promise { - const spOpts: ISPHttpClientOptions = { - headers: { - Accept: "application/json;odata.metadata=minimal", - "Content-type": "application/json;odata=verbose", - }, - body: `{'actor': { 'ActorType':1, 'ContentUri':'${fileUrl}', 'Id':null}}`, - }; - - const value = spHttpClient - .post( - `${siteUrl}/_api/social.following/isfollowed`, - SPHttpClient.configurations.v1, - spOpts - ) - .then((response: SPHttpClientResponse): Promise<{ - value: boolean; - }> => { - // Access properties of the response object. - console.log(`Status code: ${response.status}`); - console.log(`Status text: ${response.statusText}`); - - //response.json() returns a promise so you get access to the json in the resolve callback. - return response.json(); - /* response.json().then((responseJSON: JSON) => { - console.log(responseJSON); - });*/ - }) - .then((item: { value: boolean }) => { - return item.value; - }); - return value; - } - - public async follow( - spHttpClient: SPHttpClient, - fileUrl: string, - siteUrl: string - ): Promise { - const spOpts: ISPHttpClientOptions = { - headers: { - Accept: "application/json;odata.metadata=minimal", - "Content-type": "application/json;odata=verbose", - }, - body: `{'actor': { 'ActorType':1, 'ContentUri':'${fileUrl}', 'Id':null}}`, - }; - const value = await spHttpClient - .post( - `${siteUrl}/_api/social.following/follow`, - SPHttpClient.configurations.v1, - spOpts - ) - .then((response: SPHttpClientResponse): Promise => { - // Access properties of the response object. - console.log(`Status code: ${response.status}`); - console.log(`Status text: ${response.statusText}`); - - return response.json(); - }) - .then((Item: any) => { - return Item.value; - }); - if (value === 0) { - return true; - } else { - return false; - } - } - - public async stopfollowing( - spHttpClient: SPHttpClient, - fileUrl: string, - siteUrl: string - ): Promise { - const spOpts: ISPHttpClientOptions = { - headers: { - Accept: "application/json;odata.metadata=minimal", - "Content-type": "application/json;odata=verbose", - }, - body: `{'actor': { 'ActorType':1, 'ContentUri':'${fileUrl}', 'Id':null}}`, - }; - const value = await spHttpClient - .post( - `${siteUrl}/_api/social.following/stopfollowing`, - SPHttpClient.configurations.v1, - spOpts - ) - .then((response: SPHttpClientResponse) => { - // Access properties of the response object. - console.log(`Status code: ${response.status}`); - console.log(`Status text: ${response.statusText}`); - return true; - }); - return value; - } - public async followed( - spHttpClient: SPHttpClient, - siteUrl: string - ): Promise { - const spOpts: ISPHttpClientOptions = { - headers: { - Accept: "application/json;odata.metadata=minimal", - "Content-type": "application/json;odata=verbose", - }, - }; - const values = spHttpClient - .post( - `${siteUrl}/_api/social.following/my/followed(types=2)`, - SPHttpClient.configurations.v1, - spOpts - ) - .then((response: SPHttpClientResponse): Promise< - MicrosoftGraph.DriveItem[] - > => { - // Access properties of the response object. - console.log(`Status code: ${response.status}`); - console.log(`Status text: ${response.statusText}`); - - //response.json() returns a promise so you get access to the json in the resolve callback. - return response.json(); - }) - .then((Items:any) => { - let Values: MicrosoftGraph.DriveItem[] = []; - Items.value.forEach((element) => { - Values.push({ - webUrl: decodeURIComponent(element.Uri), - name: element.Name, - }); - }); - return Values; - }); - return values; - } - } \ No newline at end of file diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx index 8d775d104..fd30cf910 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx @@ -4,8 +4,9 @@ import styles from './FollowDocumentWebPart.module.scss'; import { IFollowDocumentWebPartProps } from './IFollowDocumentWebPartProps'; import { IFollowDocumentWebPartState } from './IFollowDocumentWebPartState'; import { FollowDocumentGrid } from '../components/followDocumentGrid/index'; -import Rest from '../Service/Rest'; import Graph from "../Service/GraphService"; +import { FollowDocument } from "../models/followDocument"; +import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; // Used to render list grid import { @@ -62,11 +63,222 @@ export default class FollowDocumentWebPart extends React.Component { + //Order by Date + Items = Items.sort((a, b) => { + return b.followedDateTime.getTime() - a.followedDateTime.getTime(); + }); + let uniq = {}; + let group: Array = new Array(); + //Remove duplicated from array + let uniqueArray = []; + uniqueArray = Items.filter(obj => !uniq[obj.WebUrl] && (uniq[obj.WebUrl] = true)); + group.push({ key: '0', text: 'All Sites' }); + uniqueArray.forEach(Item => { + group.push({ + key: Item.WebUrl, + text: "Site: " + Item.WebName, + }); + }); + this.setState({ + Items: Items, + ItemsSearch: Items, + ItemsGroup: group, + visible: false, + }); + }); + } + /********************************************************************** */ + private getFollowDocuments = async (followDocuments: FollowDocument[]): Promise => { + const graphService: Graph = new Graph(); + let graphData: any = []; + graphData = await graphService.getGraphContent(`https://graph.microsoft.com/v1.0/me/drive/following?$select=id,name,webUrl,parentReference,followed&Top=1000`, this.props.context); + graphData.value.forEach(data => { + + let followDocument: FollowDocument = { + ItemId: data.id, + Title: data.name, + WebFileUrl: data.webUrl, + DriveId: data.parentReference.driveId, + followedDateTime: new Date(data.followed.followedDateTime), + } as FollowDocument; + this.GetIcon(data.name).then(icon => { + followDocument.IconUrl = this.props.context.pageContext.web.absoluteUrl + "/_layouts/images/" + icon; + }); + followDocuments.push(followDocument); + }); + followDocuments = await this.getList(followDocuments); + return followDocuments; + } + + private getList = async (followDocuments: FollowDocument[]): Promise => { + let items: FollowDocument[] = []; + const graphService: Graph = new Graph(); + const initialized = await graphService.initialize(this.props.context.serviceScope); + if (initialized) { + const requests = this.getBatchRequest(followDocuments, "/me/drives/{driveId}/list?select=id,webUrl,parentReference", "GET"); + for (let index = 0; index < requests.length; index++) { + const graphData: any = await graphService.postGraphContent("https://graph.microsoft.com/v1.0/$batch", requests[index]); + graphData.responses.forEach((data: any) => { + followDocuments.forEach((followDocument: FollowDocument) => { + let driveId: string = decodeURI(data.body["@odata.context"].substring( + data.body["@odata.context"].indexOf("drives('") + 8, + data.body["@odata.context"].lastIndexOf("'") + )); + if (followDocument.DriveId === driveId && followDocument.Folder === undefined) { + followDocument.ListId = data.body.id; + followDocument.Folder = data.body.webUrl; + followDocument.ItemProperties = data.body.webUrl + "/Forms/dispForm.aspx?ID="; + followDocument.SiteId = data.body.parentReference.siteId; + items.push(followDocument); + } + }); + }); + + } + } + followDocuments = await this.getDriveItem(items); + return followDocuments; } + private getDriveItem = async (followDocuments: FollowDocument[]): Promise => { + const graphService: Graph = new Graph(); + let items: FollowDocument[] = []; + const initialized = await graphService.initialize(this.props.context.serviceScope); + if (initialized) { + const requests = this.getBatchRequest(followDocuments, "/me/drives/{driveId}/items/{ItemID}?$select=id,content.downloadUrl,ListItem&expand=ListItem(select=id,webUrl),thumbnails(select=large)", "GET"); + for (let index = 0; index < requests.length; index++) { + const graphData: any = await graphService.postGraphContent("https://graph.microsoft.com/v1.0/$batch", requests[index]); + graphData.responses.forEach((data: any) => { + followDocuments.forEach((followDocument: FollowDocument) => { + + if (followDocument.ItemId === data.body.id && followDocument.Url === undefined) { + followDocument.id = data.body.listItem.id; + followDocument.Url = data.body.listItem.webUrl; + followDocument.ItemProperties = followDocument.ItemProperties + data.body.listItem.id; + followDocument.DownloadFile = data.body["@microsoft.graph.downloadUrl"]; + followDocument.Thumbnail = data.body.thumbnails.length > 0 ? data.body.thumbnails[0].large.url : ""; + items.push(followDocument); + } + }); + }); + } + followDocuments = await this.getWeb(items); + return followDocuments; + } + } + + private getWeb = async (followDocuments: FollowDocument[]): Promise => { + const graphService: Graph = new Graph(); + let items: FollowDocument[] = []; + const initialized = await graphService.initialize(this.props.context.serviceScope); + if (initialized) { + const requests = this.getBatchRequest(followDocuments, "/sites/{SiteId}?$select=id,siteCollection,webUrl,name,displayName", "GET"); + for (let index = 0; index < requests.length; index++) { + const graphData = await graphService.postGraphContent("https://graph.microsoft.com/v1.0/$batch", requests[index]); + graphData.responses.forEach((data: any) => { + followDocuments.forEach((followDocument: FollowDocument) => { + if (followDocument.SiteId === data.body.id && followDocument.Domain === undefined) { + followDocument.Domain = data.body.siteCollection.hostname; + followDocument.WebUrl = data.body.webUrl; + followDocument.WebName = data.body.displayName; + followDocument.documentCardActions = [ + { + iconProps: { iconName: 'TeamsLogo' }, + onClick: this.onActionTeamsClick.bind(this, followDocument), + ariaLabel: 'Send to Teams', + }, + { + iconProps: { iconName: 'FabricFolder' }, + onClick: this.onActionFolderClick.bind(this, followDocument), + ariaLabel: 'open Folder', + }, + { + iconProps: { iconName: 'FavoriteStarFill' }, + onClick: this.onActionUnfollowClick.bind(this, followDocument), + ariaLabel: 'Unfollow Document', + }, + { + iconProps: { iconName: 'Info' }, + onClick: this.onActionPropertiesClick.bind(this, followDocument), + ariaLabel: 'Document info', + }, + { + iconProps: { iconName: 'DocumentSearch' }, + onClick: this.onActionPanelClick.bind(this, followDocument), + ariaLabel: 'Preview', + }, + + ]; + items.push(followDocument); + } + }); + }); + return items; + } + } + + } + + public GetIcon = async (name: string): Promise => { + var url = `${this.props.context.pageContext.web.absoluteUrl}/_api/web/maptoicon(filename='${name}',%20progid='',%20size=0)`; + const value = await this.props.context.spHttpClient.get(url, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse): Promise<{ + value: string; + }> => { + return response.json(); + }) + .then((item: { value: string }) => { + return item.value; + }); + + return value; + } + + public getBatchRequest = (followDocuments: FollowDocument[], graphQuery: string, method: string) => { + let HeaderDriveItemsId = { + "requests": [] + }; + let count = 1; + let Items = []; + followDocuments.forEach((element, index) => { + if (count < 21) { + HeaderDriveItemsId.requests.push({ + "url": graphQuery.replace("{driveId}", element.DriveId).replace("{ItemID}", element.ItemId).replace("{SiteId}", element.SiteId), + "method": method, + "id": count + }); + count++; + } else if (count === 21) { + Items.push(HeaderDriveItemsId); + HeaderDriveItemsId = { + "requests": [] + }; + count = 1; + HeaderDriveItemsId.requests.push({ + "url": graphQuery.replace("{driveId}", element.DriveId).replace("{ItemID}", element.ItemId).replace("{SiteId}", element.SiteId), + "method": method, + "id": count + }); + count++; + } + if (index === followDocuments.length - 1) { + Items.push(HeaderDriveItemsId); + HeaderDriveItemsId = { + "requests": [] + }; + count = 1; + } + }); + return Items; + } + + /************************************************************************************* */ + + + + //get Web Name and Web Url of Document private getSearchWebID = async (graphData: any[], webs: any[]): Promise => { const graphService: Graph = new Graph(); @@ -107,7 +319,7 @@ export default class FollowDocumentWebPart extends React.Component): void => { + private onActionTeamsClick = (action: FollowDocument, ev: React.SyntheticEvent): void => { const dialog: FollowDocumentDialog = new FollowDocumentDialog(); dialog.initializedTeams(action, this.props.context, followType.SendTeams); @@ -115,41 +327,12 @@ export default class FollowDocumentWebPart extends React.Component => { - const graphService: Graph = new Graph(); - const initialized = await graphService.initialize(this.props.context.serviceScope); - if (initialized) { - const HeaderListId = { - "requests": [ - { - "entityTypes": [ - "list" - ], - "query": { - "queryString": "ListID:" + ListId + "" - }, - "fields": [ - "webUrl" - ] - } - ] - }; - const tmpFileID = await graphService.postGraphContent("https://graph.microsoft.com/beta/search/query", HeaderListId); - console.log(tmpFileID); - return tmpFileID.value[0].hitsContainers[0].hits[0].resource.webUrl.substring(0, tmpFileID.value[0].hitsContainers[0].hits[0].resource.webUrl.lastIndexOf("/")); - } - } - private getListItemID = async (ListID, ItemID) => { - const _ListId = await this.getSearchListItemID(ListID); - const dialog: FollowDocumentDialog = new FollowDocumentDialog(); - dialog.initialize(_ListId + "/dispForm.aspx?ID=" + ItemID, followType.ViewPropreties); - } - - private _showPanel = (Url: string, Title: string): void => { + private _showPanel = (followDocument: FollowDocument): void => { this._renderPanelComponent({ + FollowDocument: followDocument, context: this.props.context, - url: Url, - filename: Title, + url: followDocument.Url, + filename: followDocument.Title, isOpen: true, }); } @@ -159,15 +342,16 @@ export default class FollowDocumentWebPart extends React.Component): void => { + private onActionPropertiesClick = (action: FollowDocument, ev: React.SyntheticEvent): void => { //Get Document Display Form List - this.getListItemID(action.fields.ListId.replace('{', '').replace('}', ''), action.fields.ItemId); + const dialog: FollowDocumentDialog = new FollowDocumentDialog(); + dialog.initialize(action.ItemProperties, followType.ViewPropreties); ev.stopPropagation(); ev.preventDefault(); } - private onActionFolderClick = (action: any, ev: React.SyntheticEvent): void => { - window.open(action.fields.Url.replace(action.fields.Title, ""), "_blank"); + private onActionFolderClick = (action: FollowDocument, ev: React.SyntheticEvent): void => { + window.open(action.Url.replace(action.Title, ""), "_blank"); ev.stopPropagation(); ev.preventDefault(); } @@ -175,159 +359,43 @@ export default class FollowDocumentWebPart extends React.Component) => { + private onActionUnfollowClick = async (action: FollowDocument, ev: React.SyntheticEvent) => { ev.stopPropagation(); ev.preventDefault(); const dialog: FollowDocumentDialog = new FollowDocumentDialog(); dialog._followTypeDialog = followType.Unfollow; - dialog._filename = action.fields.Title; + dialog._filename = action.Title; dialog.show().then(async () => { if (dialog._followDocumentState) { - const restService: Rest = new Rest(); - const Status = await restService.stopfollowing( - this.props.context.spHttpClient, - action.fields.Url, - this.props.context.pageContext.web.absoluteUrl, - ); - if (Status) { - dialog._followDocumentState = false; - this.getListItems(); + const graphService: Graph = new Graph(); + const initialized = await graphService.initialize(this.props.context.serviceScope); + if (initialized) { + const graphData: any = await graphService.postGraphContent(`https://graph.microsoft.com/v1.0/drives/${action.DriveId}/items/${action.ItemId}/unfollow`, ""); + if (graphData === undefined) { + dialog._followDocumentState = false; + this.getListItems(); + } } } }); } - private onActionPanelClick = async (action: any, ev: React.SyntheticEvent) => { - this._showPanel(action.fields.Url, action.fields.Title); + private onActionPanelClick = async (action: FollowDocument, ev: React.SyntheticEvent) => { + this._showPanel(action); ev.stopPropagation(); ev.preventDefault(); } - private getGraphFollowedDocs = async () => { - const GraphService: Graph = new Graph(); - let DriveItem: any = []; - - if (this.state.siteId === null) { - let graphData: any = await GraphService.getGraphContent("https://graph.microsoft.com/v1.0/me/drive/list", this.props.context); - this._siteId = graphData.parentReference.siteId; - DriveItem = await this.getListID(graphData.parentReference.siteId); - } else { - if (this.state.listId === null) { - DriveItem = await this.getListID(this.state.siteId); - } else { - DriveItem = await this.getFollowDocuments(this.state.siteId, this.state.listId); - - } - } - let items = []; - DriveItem.forEach(element => { - if (element.fields.IconUrl.indexOf("lg_iczip.gif") > -1) { - element.fields.IconUrl = element.fields.IconUrl.replace("lg_iczip.gif", "lg_iczip.png"); - } - if (element.fields.IconUrl.indexOf("lg_icmsg.png") > -1) { - element.fields.IconUrl = element.fields.IconUrl.replace("lg_icmsg.png", "lg_icmsg.gif"); - } - items.push({ - thumbnail: element.previewImg, - title: element.fields.Title, - profileImageSrc: element.fields.IconUrl, - url: (element.fields.ServerUrlProgid === undefined ? element.fields.Url : element.fields.ServerUrlProgid.substring(1)), - webName: element.WebName, - webUrl: element.WebUrl, - documentCardActions: [ - { - iconProps: { iconName: 'TeamsLogo' }, - onClick: this.onActionTeamsClick.bind(this, element), - ariaLabel: 'Send to Teams', - }, - { - iconProps: { iconName: 'FabricFolder' }, - onClick: this.onActionFolderClick.bind(this, element), - ariaLabel: 'open Folder', - }, - { - iconProps: { iconName: 'FavoriteStarFill' }, - onClick: this.onActionUnfollowClick.bind(this, element), - ariaLabel: 'Unfollow Document', - }, - { - iconProps: { iconName: 'Info' }, - onClick: this.onActionPropertiesClick.bind(this, element), - ariaLabel: 'Document info', - }, - { - iconProps: { iconName: 'DocumentSearch' }, - onClick: this.onActionPanelClick.bind(this, element), - ariaLabel: 'Preview', - }, - - ] - }); - - }); - let uniq = {}; - let group: Array = new Array(); - //Remove duplicated from array - let uniqueArray = []; - uniqueArray = items.filter(obj => !uniq[obj.webUrl] && (uniq[obj.webUrl] = true)); - group.push({ key: '0', text: 'All Sites' }); - uniqueArray.forEach(element => { - group.push({ - key: element.webUrl, - text: "Site: " + element.webName, - }); - }); - this.setState({ - Items: items, - ItemsSearch: items, - ItemsGroup: group, - visible: false, - siteId: this._siteId, - listId: this._listId - }); - - } - private getListID = async (siteId: string): Promise => { - const GraphService: Graph = new Graph(); - let graphData: any = await GraphService.getGraphContent(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists?$select=id&$filter=displayName eq 'Social'`, this.props.context); - this._listId = graphData.value[0].id; - const DriveItem: string = await this.getFollowDocuments(siteId, graphData.value[0].id); - return DriveItem; - } - - private getFollowDocuments = async (siteId: string, listId: string): Promise => { - const GraphService: Graph = new Graph(); - let graphData: any = []; - graphData = await GraphService.getGraphContent(`https://graph.microsoft.com/v1.0/sites/${siteId}/Lists/${listId}/items?expand=fields(select=ItemId,ListId,SiteId,webId,Title,Url,ServerUrlProgid,IconUrl,File_x0020_Type.progid)&$filter=fields/ItemId gt -1`, this.props.context); - graphData.value = graphData.value.sort((a, b) => { - return b.id - a.id; - }); - - //Get Web site Name - graphData = await this.getFollowDocumentsWebName(graphData); - return graphData; - } - - private getFollowDocumentsWebName = async (graphData) => { - let _webs = []; - graphData.value.forEach(element => { - if (_webs.indexOf(element.fields.WebId) === -1) { - _webs.push(element.fields.WebId); - } - }); - graphData = await this.getSearchWebID(graphData.value, _webs); - return graphData; - } public render(): React.ReactElement { //Filter Search Text const checkSearchDrive = (SearchQuery: string) => { let items = []; if (this._selectedGroup === "0") { - items = this.state.Items.filter(item => (item.title.toLowerCase().indexOf(SearchQuery.toLowerCase()) > -1)); + items = this.state.Items.filter(item => (item.Title.toLowerCase().indexOf(SearchQuery.toLowerCase()) > -1)); } else { - items = this.state.Items.filter(item => (item.title.toLowerCase().indexOf(SearchQuery.toLowerCase()) > -1 && item.webUrl.toLowerCase().indexOf(this._selectedGroup.toLowerCase()) > -1)); + items = this.state.Items.filter(item => (item.Title.toLowerCase().indexOf(SearchQuery.toLowerCase()) > -1 && item.WebUrl.toLowerCase().indexOf(this._selectedGroup.toLowerCase()) > -1)); } this.setState({ ItemsSearch: items, @@ -339,7 +407,7 @@ export default class FollowDocumentWebPart extends React.Component (item.webUrl.toLowerCase().indexOf(this._selectedGroup.toLowerCase()) > -1)); + items = this.state.Items.filter(item => (item.WebUrl.toLowerCase().indexOf(this._selectedGroup.toLowerCase()) > -1)); } this.setState({ ItemsSearch: items, @@ -353,7 +421,7 @@ export default class FollowDocumentWebPart extends React.Component item.webUrl.toLowerCase().indexOf(selectedOption.key.toString().toLowerCase()) > -1); + const items = this.state.Items.filter(item => item.WebUrl.toLowerCase().indexOf(selectedOption.key.toString().toLowerCase()) > -1); this.setState({ ItemsSearch: items, }); @@ -405,31 +473,26 @@ export default class FollowDocumentWebPart extends React.Component this._onRenderGridItem(item, finalSize, isCompact)} + onRenderGridItem={(item, finalSize: ISize, isCompact: boolean) => this._onRenderGridItem(item, finalSize, isCompact)} /> ); } - private _onRenderGridItem = (item: any, finalSize: ISize, isCompact: boolean): JSX.Element => { + private _onRenderGridItem = (item: FollowDocument, finalSize: ISize, isCompact: boolean): JSX.Element => { - return
+ return
-
window.open(item.url, '_blank')}> - +
window.open(item.WebFileUrl, '_blank')}> +
- {!isCompact && window.open(item.webUrl, '_blank')} />} + {!isCompact && window.open(item.WebUrl, '_blank')} />} diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/IFollowDocumentWebPartState.ts b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/IFollowDocumentWebPartState.ts index 06f4708e9..487b35f53 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/IFollowDocumentWebPartState.ts +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/IFollowDocumentWebPartState.ts @@ -1,8 +1,9 @@ import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; +import { FollowDocument } from "../models/followDocument"; export interface IFollowDocumentWebPartState { siteId?: string; listId?: string; - Items: any; + Items: FollowDocument[]; ItemsSearch?: any; ItemsGroup?: IDropdownOption[]; previewImgUrl:string; diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentDialog/followDocumentDialog.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentDialog/followDocumentDialog.tsx index 3ef1406c1..865c8abb7 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentDialog/followDocumentDialog.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentDialog/followDocumentDialog.tsx @@ -8,14 +8,15 @@ import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog'; import { DialogContent, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { FollowDocumentProperties } from '../followDocumentProperties/followDocumentProperties'; import { FollowDocumentSendMessage } from '../followDocumentSendMessage/followDocumentSendMessage'; +import { FollowDocument } from '../../models/followDocument'; export default class FollowDocumentDialog extends BaseDialog { public _followDocumentState: boolean = false; private _webUrl: string; - public _filename:string; - private _context:WebPartContext; + public _filename: string; + private _context: WebPartContext; public _followTypeDialog: followType; - public _fileInfo: any; + public _fileInfo: FollowDocument; public return: (string) => void; @@ -24,7 +25,7 @@ export default class FollowDocumentDialog extends BaseDialog { this._followTypeDialog = type; this.show(); } - public async initializedTeams(fileInfo: any,context:WebPartContext, type: followType) { + public async initializedTeams(fileInfo: FollowDocument, context: WebPartContext, type: followType) { this._context = context; this._fileInfo = fileInfo; this._followTypeDialog = type; diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentPreview/IfollowDocumentPreviewProps.ts b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentPreview/IfollowDocumentPreviewProps.ts index f5ed576a9..b8ea32561 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentPreview/IfollowDocumentPreviewProps.ts +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentPreview/IfollowDocumentPreviewProps.ts @@ -1,9 +1,11 @@ import { WebPartContext } from "@microsoft/sp-webpart-base"; +import { FollowDocument } from "../../models/followDocument"; export interface IfollowDocumentPreviewProps { isOpen: boolean; url?:string; filename?:string; context: WebPartContext; visible?:boolean; + FollowDocument: FollowDocument; } \ No newline at end of file diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentPreview/followDocumentPreview.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentPreview/followDocumentPreview.tsx index ef4647ce5..4a29aa004 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentPreview/followDocumentPreview.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentPreview/followDocumentPreview.tsx @@ -33,21 +33,10 @@ export class followDocumentPreview extends React.Component void; url: string; context: WebPartContext; - fileInfo:any; + fileInfo: FollowDocument; } \ No newline at end of file diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentSendMessage/followDocumentSendMessage.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentSendMessage/followDocumentSendMessage.tsx index 2b4756183..eba3e67d9 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentSendMessage/followDocumentSendMessage.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/followDocumentSendMessage/followDocumentSendMessage.tsx @@ -9,6 +9,7 @@ import { ITag, } from "office-ui-fabric-react/lib/Pickers"; import * as AdaptiveCards from "adaptivecards"; import Graph from "../../Service/GraphService"; import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; +import { FollowDocument } from '../../models/followDocument'; export class FollowDocumentSendMessage extends React.Component { private card: any; @@ -47,7 +48,7 @@ export class FollowDocumentSendMessage extends React.Component${this.props.fileInfo.fields.Title}` + "content": this._acContainer.innerHTML + `${this.props.fileInfo.Title}` } }; const getresult = await graphService.postGraphContent(`https://graph.microsoft.com/v1.0/teams/${this.state.selectedTeamId}/channels/${this.state.selectedTeamChannelId}/messages`, HeadersendMessage); @@ -67,7 +68,7 @@ export class FollowDocumentSendMessage extends React.Component; + preview?: string; + Description?: string; + followedDateTime?: Date; +} From c1d6e369e38abdc4359702a7b3ebbab0a10df43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lage?= Date: Fri, 26 Nov 2021 09:06:07 +0100 Subject: [PATCH 2/7] update large DS --- .../components/FollowDocumentWebPart.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx index fd30cf910..c58395dfc 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx @@ -94,21 +94,23 @@ export default class FollowDocumentWebPart extends React.Component { + if (graphData.value !== undefined) { + graphData.value.forEach(data => { - let followDocument: FollowDocument = { - ItemId: data.id, - Title: data.name, - WebFileUrl: data.webUrl, - DriveId: data.parentReference.driveId, - followedDateTime: new Date(data.followed.followedDateTime), - } as FollowDocument; - this.GetIcon(data.name).then(icon => { - followDocument.IconUrl = this.props.context.pageContext.web.absoluteUrl + "/_layouts/images/" + icon; + let followDocument: FollowDocument = { + ItemId: data.id, + Title: data.name, + WebFileUrl: data.webUrl, + DriveId: data.parentReference.driveId, + followedDateTime: new Date(data.followed.followedDateTime), + } as FollowDocument; + this.GetIcon(data.name).then(icon => { + followDocument.IconUrl = this.props.context.pageContext.web.absoluteUrl + "/_layouts/images/" + icon; + }); + followDocuments.push(followDocument); }); - followDocuments.push(followDocument); - }); - followDocuments = await this.getList(followDocuments); + followDocuments = await this.getList(followDocuments); + } return followDocuments; } From f82e2ad740474460b1a95f46948f1f18f50eb24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lage?= Date: Fri, 26 Nov 2021 10:08:09 +0100 Subject: [PATCH 3/7] improve query --- .../components/FollowDocumentWebPart.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx index c58395dfc..43aa410fd 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx @@ -119,7 +119,10 @@ export default class FollowDocumentWebPart extends React.Component !uniq[obj.DriveId] && (uniq[obj.DriveId] = true)); + const requests = this.getBatchRequest(uniqueArray, "/me/drives/{driveId}/list?select=id,webUrl,parentReference", "GET"); for (let index = 0; index < requests.length; index++) { const graphData: any = await graphService.postGraphContent("https://graph.microsoft.com/v1.0/$batch", requests[index]); graphData.responses.forEach((data: any) => { @@ -177,12 +180,15 @@ export default class FollowDocumentWebPart extends React.Component !uniq[obj.SiteId] && (uniq[obj.SiteId] = true)); + const requests = this.getBatchRequest(uniqueArray, "/sites/{SiteId}?$select=id,siteCollection,webUrl,name,displayName", "GET"); for (let index = 0; index < requests.length; index++) { const graphData = await graphService.postGraphContent("https://graph.microsoft.com/v1.0/$batch", requests[index]); graphData.responses.forEach((data: any) => { followDocuments.forEach((followDocument: FollowDocument) => { - if (followDocument.SiteId === data.body.id && followDocument.Domain === undefined) { + if (followDocument.SiteId === data.body.id && (followDocument.Domain === undefined || followDocument.Domain === "")) { followDocument.Domain = data.body.siteCollection.hostname; followDocument.WebUrl = data.body.webUrl; followDocument.WebName = data.body.displayName; From 8abe025f98f42aac72d0958f764797e3d9a721f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lage?= Date: Fri, 26 Nov 2021 10:48:11 +0100 Subject: [PATCH 4/7] correct filter of files. --- .../followDocumentWebPart/components/FollowDocumentWebPart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx index 43aa410fd..5dd572da3 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx @@ -429,7 +429,7 @@ export default class FollowDocumentWebPart extends React.Component item.WebUrl.toLowerCase().indexOf(selectedOption.key.toString().toLowerCase()) > -1); + const items = this.state.Items.filter(item => item.WebUrl.toLowerCase() === selectedOption.key.toString().toLowerCase()); this.setState({ ItemsSearch: items, }); From 0981e10bad50edb2e6ff6656ce1506ca5e90017e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lage?= Date: Sat, 27 Nov 2021 02:28:24 +0100 Subject: [PATCH 5/7] update folder path --- .../components/FollowDocumentWebPart.tsx | 47 +------------------ 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx index 5dd572da3..09befcf06 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx @@ -131,9 +131,8 @@ export default class FollowDocumentWebPart extends React.Component 0 ? data.body.thumbnails[0].large.url : ""; @@ -284,49 +284,6 @@ export default class FollowDocumentWebPart extends React.Component => { - const graphService: Graph = new Graph(); - const initialized = await graphService.initialize(this.props.context.serviceScope); - let queryString: string = ""; - for (let index = 0; index < webs.length; index++) { - if (index === 0) { - queryString += "WebId:" + webs[index].replace('{', '').replace('}', ''); - } else { - queryString += " OR WebId:" + webs[index].replace('{', '').replace('}', '') + " "; - } - } - if (initialized) { - const HeaderWeb = { - "requests": [ - { - "entityTypes": [ - "site" - ], - "query": { - "queryString": "" + queryString + "", - } - } - ] - }; - //Retrieve webNames - const tmpWebs = await graphService.postGraphContent("https://graph.microsoft.com/beta/search/query", HeaderWeb); - graphData.forEach(element => { - tmpWebs.value[0].hitsContainers[0].hits.forEach(Webelement => { - if (element.fields.WebId.replace('{', '').replace('}', '') === Webelement.resource.id.split(/[, ]+/).pop().toUpperCase()) { - element.WebName = Webelement.resource.name; - element.WebUrl = Webelement.resource.webUrl; - } - } - ); - }); - return graphData; - } - } - private onActionTeamsClick = (action: FollowDocument, ev: React.SyntheticEvent): void => { const dialog: FollowDocumentDialog = new FollowDocumentDialog(); From 29af2b0934594e18af40e879ebbadfc286a47ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lage?= Date: Sat, 27 Nov 2021 15:10:35 +0100 Subject: [PATCH 6/7] update to use large images. --- .../followDocumentWebPart/components/FollowDocumentWebPart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx index 09befcf06..653bc6ad4 100644 --- a/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx +++ b/samples/react-follow-document/src/webparts/followDocumentWebPart/components/FollowDocumentWebPart.tsx @@ -105,7 +105,7 @@ export default class FollowDocumentWebPart extends React.Component { - followDocument.IconUrl = this.props.context.pageContext.web.absoluteUrl + "/_layouts/images/" + icon; + followDocument.IconUrl = (this.props.context.pageContext.web.absoluteUrl + "/_layouts/15/images/lg_" + icon).replace("lg_iczip.gif", "lg_iczip.png").replace("lg_icmsg.png", "lg_icmsg.gif"); }); followDocuments.push(followDocument); }); From 20a87b66db4c980a292bce54c665ab30f47af412 Mon Sep 17 00:00:00 2001 From: Hugo Bernier Date: Thu, 2 Dec 2021 02:30:21 -0500 Subject: [PATCH 7/7] Updated sample.json --- samples/react-follow-document/assets/sample.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/react-follow-document/assets/sample.json b/samples/react-follow-document/assets/sample.json index 87815b720..cd90db698 100644 --- a/samples/react-follow-document/assets/sample.json +++ b/samples/react-follow-document/assets/sample.json @@ -9,7 +9,7 @@ "identify/follow user key documents from all Tenant and easily access them in Modern Pages and Microsoft Teams" ], "creationDateTime": "2021-06-21", - "updateDateTime": "2021-06-21", + "updateDateTime": "2021-11-25", "products": [ "SharePoint", "Office"