Merge pull request #2149 from aaclage/Feature/FollowDocumentGraph

updated to use Microsoft Graph follow
This commit is contained in:
Hugo Bernier 2021-12-02 02:30:56 -05:00 committed by GitHub
commit 78ad673966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 328 additions and 410 deletions

View File

@ -2,7 +2,7 @@
## Summary ## 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. 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. - Microsoft Team integration with personal/Tab App that allow user focus on key Documents.
Usage of following Technologies: 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 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 Graph queries using [Graph explorer](https://developer.microsoft.com/en-us/graph/graph-explorer) - 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/) - Usage of [adaptive cards](https://adaptivecards.io/)
- Microsoft Teams integration with following option [TeamsTab, TeamsPersonalApp] - 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'
o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Files.Read.All' 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 '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 'Team.ReadBasic.All'
o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Channel.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' 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 Version|Date|Comments
-------|----|-------- -------|----|--------
1.0|June 22, 2021|Initial release 1.0|June 22, 2021|Initial release
2.0|November 25, 2021|Change to use Microsoft Graph Follow
## Minimal Path to Awesome ## 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: 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. - 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. - Option to unfollow documents individually.
- Integration with other services of Office 365 such us (Preview, Microsoft Team Messages). - Integration with other services of Office 365 such us (Preview, Microsoft Team Messages).

View File

@ -9,7 +9,7 @@
"identify/follow user key documents from all Tenant and easily access them in Modern Pages and Microsoft Teams" "identify/follow user key documents from all Tenant and easily access them in Modern Pages and Microsoft Teams"
], ],
"creationDateTime": "2021-06-21", "creationDateTime": "2021-06-21",
"updateDateTime": "2021-06-21", "updateDateTime": "2021-11-25",
"products": [ "products": [
"SharePoint", "SharePoint",
"Office" "Office"

View File

@ -14,25 +14,40 @@
"termsOfUseUrl": "", "termsOfUseUrl": "",
"mpnId": "" "mpnId": ""
}, },
"webApiPermissionRequests": [{ "webApiPermissionRequests": [
"resource": "Microsoft Graph", {
"scope": "Files.Read" "resource": "Microsoft Graph",
}, { "scope": "Files.Read"
"resource": "Microsoft Graph", },
"scope": "Files.Read.All" {
}, { "resource": "Microsoft Graph",
"resource": "Microsoft Graph", "scope": "Files.ReadWrite.All"
"scope": "Sites.Read.All" },
}, { {
"resource": "Microsoft Graph", "resource": "Microsoft Graph",
"scope": "Team.ReadBasic.All" "scope": "Sites.ReadWrite.All"
}, { },
"resource": "Microsoft Graph", {
"scope": "Channel.ReadBasic.All" "resource": "Microsoft Graph",
}, { "scope": "Files.Read.All"
"resource": "Microsoft Graph", },
"scope": "ChannelMessage.Send" {
}] "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": { "paths": {
"zippedPackage": "solution/follow-document-web-part.sppkg" "zippedPackage": "solution/follow-document-web-part.sppkg"

View File

@ -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<boolean> {
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<boolean> {
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<number> => {
// 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<boolean> {
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<MicrosoftGraph.DriveItem[]> {
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;
}
}

View File

@ -4,8 +4,9 @@ import styles from './FollowDocumentWebPart.module.scss';
import { IFollowDocumentWebPartProps } from './IFollowDocumentWebPartProps'; import { IFollowDocumentWebPartProps } from './IFollowDocumentWebPartProps';
import { IFollowDocumentWebPartState } from './IFollowDocumentWebPartState'; import { IFollowDocumentWebPartState } from './IFollowDocumentWebPartState';
import { FollowDocumentGrid } from '../components/followDocumentGrid/index'; import { FollowDocumentGrid } from '../components/followDocumentGrid/index';
import Rest from '../Service/Rest';
import Graph from "../Service/GraphService"; import Graph from "../Service/GraphService";
import { FollowDocument } from "../models/followDocument";
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
// Used to render list grid // Used to render list grid
import { import {
@ -62,52 +63,228 @@ export default class FollowDocumentWebPart extends React.Component<IFollowDocume
visible: true, visible: true,
}); });
} }
//Load using Graph let followDocuments: FollowDocument[] = [];
this.getGraphFollowedDocs(); this.getFollowDocuments(followDocuments).then((Items: FollowDocument[]) => {
//Order by Date
Items = Items.sort((a, b) => {
return b.followedDateTime.getTime() - a.followedDateTime.getTime();
});
let uniq = {};
let group: Array<IDropdownOption> = new Array<IDropdownOption>();
//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<any> => {
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);
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/15/images/lg_" + icon).replace("lg_iczip.gif", "lg_iczip.png").replace("lg_icmsg.png", "lg_icmsg.gif");
});
followDocuments.push(followDocument);
});
followDocuments = await this.getList(followDocuments);
}
return followDocuments;
} }
//get Web Name and Web Url of Document private getList = async (followDocuments: FollowDocument[]): Promise<any> => {
private getSearchWebID = async (graphData: any[], webs: any[]): Promise<any[]> => { let items: FollowDocument[] = [];
const graphService: Graph = new Graph(); const graphService: Graph = new Graph();
const initialized = await graphService.initialize(this.props.context.serviceScope); const initialized = await graphService.initialize(this.props.context.serviceScope);
let queryString: string = ""; if (initialized) {
for (let index = 0; index < webs.length; index++) { let uniq = {};
if (index === 0) { let uniqueArray = [];
queryString += "WebId:" + webs[index].replace('{', '').replace('}', ''); uniqueArray = followDocuments.filter(obj => !uniq[obj.DriveId] && (uniq[obj.DriveId] = true));
} else { const requests = this.getBatchRequest(uniqueArray, "/me/drives/{driveId}/list?select=id,webUrl,parentReference", "GET");
queryString += " OR WebId:" + webs[index].replace('{', '').replace('}', '') + " "; 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.ListId === undefined || followDocument.ListId === "")) {
followDocument.ListId = data.body.id;
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<any> => {
const graphService: Graph = new Graph();
let items: FollowDocument[] = [];
const initialized = await graphService.initialize(this.props.context.serviceScope);
if (initialized) { if (initialized) {
const HeaderWeb = { const requests = this.getBatchRequest(followDocuments, "/me/drives/{driveId}/items/{ItemID}?$select=id,content.downloadUrl,ListItem&expand=ListItem(select=id,webUrl),thumbnails(select=large)", "GET");
"requests": [ for (let index = 0; index < requests.length; index++) {
{ const graphData: any = await graphService.postGraphContent("https://graph.microsoft.com/v1.0/$batch", requests[index]);
"entityTypes": [ graphData.responses.forEach((data: any) => {
"site" followDocuments.forEach((followDocument: FollowDocument) => {
],
"query": { if (followDocument.ItemId === data.body.id && followDocument.Url === undefined) {
"queryString": "" + queryString + "", followDocument.id = data.body.listItem.id;
followDocument.Url = data.body.listItem.webUrl;
followDocument.Folder = data.body.listItem.webUrl.substring(0, data.body.listItem.webUrl.lastIndexOf("/") + 1);
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);
} }
} });
] });
}; }
//Retrieve webNames followDocuments = await this.getWeb(items);
const tmpWebs = await graphService.postGraphContent("https://graph.microsoft.com/beta/search/query", HeaderWeb); return followDocuments;
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: any, ev: React.SyntheticEvent<HTMLElement>): void => { private getWeb = async (followDocuments: FollowDocument[]): Promise<any> => {
const graphService: Graph = new Graph();
let items: FollowDocument[] = [];
const initialized = await graphService.initialize(this.props.context.serviceScope);
if (initialized) {
let uniq = {};
let uniqueArray = [];
uniqueArray = followDocuments.filter(obj => !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 || followDocument.Domain === "")) {
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<string> => {
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;
}
/************************************************************************************* */
private onActionTeamsClick = (action: FollowDocument, ev: React.SyntheticEvent<HTMLElement>): void => {
const dialog: FollowDocumentDialog = new FollowDocumentDialog(); const dialog: FollowDocumentDialog = new FollowDocumentDialog();
dialog.initializedTeams(action, this.props.context, followType.SendTeams); dialog.initializedTeams(action, this.props.context, followType.SendTeams);
@ -115,41 +292,12 @@ export default class FollowDocumentWebPart extends React.Component<IFollowDocume
ev.preventDefault(); ev.preventDefault();
} }
private getSearchListItemID = async (ListId: string): Promise<string> => { private _showPanel = (followDocument: FollowDocument): void => {
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 => {
this._renderPanelComponent({ this._renderPanelComponent({
FollowDocument: followDocument,
context: this.props.context, context: this.props.context,
url: Url, url: followDocument.Url,
filename: Title, filename: followDocument.Title,
isOpen: true, isOpen: true,
}); });
} }
@ -159,15 +307,16 @@ export default class FollowDocumentWebPart extends React.Component<IFollowDocume
ReactDom.render(element, this._panelPlaceHolder); ReactDom.render(element, this._panelPlaceHolder);
} }
private onActionPropertiesClick = (action: any, ev: React.SyntheticEvent<HTMLElement>): void => { private onActionPropertiesClick = (action: FollowDocument, ev: React.SyntheticEvent<HTMLElement>): void => {
//Get Document Display Form List //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.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} }
private onActionFolderClick = (action: any, ev: React.SyntheticEvent<HTMLElement>): void => { private onActionFolderClick = (action: FollowDocument, ev: React.SyntheticEvent<HTMLElement>): void => {
window.open(action.fields.Url.replace(action.fields.Title, ""), "_blank"); window.open(action.Url.replace(action.Title, ""), "_blank");
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} }
@ -175,159 +324,43 @@ export default class FollowDocumentWebPart extends React.Component<IFollowDocume
/** /**
* Unfollow Option * Unfollow Option
*/ */
private onActionUnfollowClick = async (action: any, ev: React.SyntheticEvent<HTMLElement>) => { private onActionUnfollowClick = async (action: FollowDocument, ev: React.SyntheticEvent<HTMLElement>) => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
const dialog: FollowDocumentDialog = new FollowDocumentDialog(); const dialog: FollowDocumentDialog = new FollowDocumentDialog();
dialog._followTypeDialog = followType.Unfollow; dialog._followTypeDialog = followType.Unfollow;
dialog._filename = action.fields.Title; dialog._filename = action.Title;
dialog.show().then(async () => { dialog.show().then(async () => {
if (dialog._followDocumentState) { if (dialog._followDocumentState) {
const restService: Rest = new Rest(); const graphService: Graph = new Graph();
const Status = await restService.stopfollowing( const initialized = await graphService.initialize(this.props.context.serviceScope);
this.props.context.spHttpClient, if (initialized) {
action.fields.Url, const graphData: any = await graphService.postGraphContent(`https://graph.microsoft.com/v1.0/drives/${action.DriveId}/items/${action.ItemId}/unfollow`, "");
this.props.context.pageContext.web.absoluteUrl, if (graphData === undefined) {
); dialog._followDocumentState = false;
if (Status) { this.getListItems();
dialog._followDocumentState = false; }
this.getListItems();
} }
} }
}); });
} }
private onActionPanelClick = async (action: any, ev: React.SyntheticEvent<HTMLElement>) => { private onActionPanelClick = async (action: FollowDocument, ev: React.SyntheticEvent<HTMLElement>) => {
this._showPanel(action.fields.Url, action.fields.Title); this._showPanel(action);
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); 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<IDropdownOption> = new Array<IDropdownOption>();
//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<string> => {
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<any> => {
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<IFollowDocumentWebPartProps> { public render(): React.ReactElement<IFollowDocumentWebPartProps> {
//Filter Search Text //Filter Search Text
const checkSearchDrive = (SearchQuery: string) => { const checkSearchDrive = (SearchQuery: string) => {
let items = []; let items = [];
if (this._selectedGroup === "0") { 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 { } 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({ this.setState({
ItemsSearch: items, ItemsSearch: items,
@ -339,7 +372,7 @@ export default class FollowDocumentWebPart extends React.Component<IFollowDocume
items = this.state.Items; items = this.state.Items;
} else { } else {
items = this.state.Items.filter(item => (item.webUrl.toLowerCase().indexOf(this._selectedGroup.toLowerCase()) > -1)); items = this.state.Items.filter(item => (item.WebUrl.toLowerCase().indexOf(this._selectedGroup.toLowerCase()) > -1));
} }
this.setState({ this.setState({
ItemsSearch: items, ItemsSearch: items,
@ -353,7 +386,7 @@ export default class FollowDocumentWebPart extends React.Component<IFollowDocume
ItemsSearch: this.state.Items, ItemsSearch: this.state.Items,
}); });
} else { } else {
const items = this.state.Items.filter(item => 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({ this.setState({
ItemsSearch: items, ItemsSearch: items,
}); });
@ -405,31 +438,26 @@ export default class FollowDocumentWebPart extends React.Component<IFollowDocume
<div className={styles.grid}> <div className={styles.grid}>
<FollowDocumentGrid <FollowDocumentGrid
items={this.state.ItemsSearch} items={this.state.ItemsSearch}
onRenderGridItem={(item: any, finalSize: ISize, isCompact: boolean) => this._onRenderGridItem(item, finalSize, isCompact)} onRenderGridItem={(item, finalSize: ISize, isCompact: boolean) => this._onRenderGridItem(item, finalSize, isCompact)}
/> />
</div> </div>
</> </>
); );
} }
private _onRenderGridItem = (item: any, finalSize: ISize, isCompact: boolean): JSX.Element => { private _onRenderGridItem = (item: FollowDocument, finalSize: ISize, isCompact: boolean): JSX.Element => {
return <div return <div className={styles.documentTile} data-is-focusable={true} aria-label={item.Title} >
className={styles.documentTile}
data-is-focusable={true}
role="listitem"
aria-label={item.title}
>
<DocumentCard <DocumentCard
type={isCompact ? DocumentCardType.compact : DocumentCardType.normal} type={isCompact ? DocumentCardType.compact : DocumentCardType.normal}
> >
<div style={{ cursor: 'pointer' }} onClick={() => window.open(item.url, '_blank')}> <div style={{ cursor: 'pointer' }} onClick={() => window.open(item.WebFileUrl, '_blank')}>
<DocumentCardImage height={100} imageFit={ImageFit.center} imageSrc={item.profileImageSrc} /> <DocumentCardImage height={100} imageFit={ImageFit.center} imageSrc={item.IconUrl} />
</div> </div>
{!isCompact && <DocumentCardLocation location={item.webName} onClick={() => window.open(item.webUrl, '_blank')} />} {!isCompact && <DocumentCardLocation location={item.WebName} onClick={() => window.open(item.WebUrl, '_blank')} />}
<DocumentCardDetails> <DocumentCardDetails>
<DocumentCardTitle <DocumentCardTitle
title={item.title} title={item.Title}
shouldTruncate={true} shouldTruncate={true}
/> />

View File

@ -1,8 +1,9 @@
import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { FollowDocument } from "../models/followDocument";
export interface IFollowDocumentWebPartState { export interface IFollowDocumentWebPartState {
siteId?: string; siteId?: string;
listId?: string; listId?: string;
Items: any; Items: FollowDocument[];
ItemsSearch?: any; ItemsSearch?: any;
ItemsGroup?: IDropdownOption[]; ItemsGroup?: IDropdownOption[];
previewImgUrl:string; previewImgUrl:string;

View File

@ -8,14 +8,15 @@ import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog';
import { DialogContent, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { DialogContent, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import { FollowDocumentProperties } from '../followDocumentProperties/followDocumentProperties'; import { FollowDocumentProperties } from '../followDocumentProperties/followDocumentProperties';
import { FollowDocumentSendMessage } from '../followDocumentSendMessage/followDocumentSendMessage'; import { FollowDocumentSendMessage } from '../followDocumentSendMessage/followDocumentSendMessage';
import { FollowDocument } from '../../models/followDocument';
export default class FollowDocumentDialog extends BaseDialog { export default class FollowDocumentDialog extends BaseDialog {
public _followDocumentState: boolean = false; public _followDocumentState: boolean = false;
private _webUrl: string; private _webUrl: string;
public _filename:string; public _filename: string;
private _context:WebPartContext; private _context: WebPartContext;
public _followTypeDialog: followType; public _followTypeDialog: followType;
public _fileInfo: any; public _fileInfo: FollowDocument;
public return: (string) => void; public return: (string) => void;
@ -24,7 +25,7 @@ export default class FollowDocumentDialog extends BaseDialog {
this._followTypeDialog = type; this._followTypeDialog = type;
this.show(); this.show();
} }
public async initializedTeams(fileInfo: any,context:WebPartContext, type: followType) { public async initializedTeams(fileInfo: FollowDocument, context: WebPartContext, type: followType) {
this._context = context; this._context = context;
this._fileInfo = fileInfo; this._fileInfo = fileInfo;
this._followTypeDialog = type; this._followTypeDialog = type;

View File

@ -1,9 +1,11 @@
import { WebPartContext } from "@microsoft/sp-webpart-base"; import { WebPartContext } from "@microsoft/sp-webpart-base";
import { FollowDocument } from "../../models/followDocument";
export interface IfollowDocumentPreviewProps { export interface IfollowDocumentPreviewProps {
isOpen: boolean; isOpen: boolean;
url?:string; url?:string;
filename?:string; filename?:string;
context: WebPartContext; context: WebPartContext;
visible?:boolean; visible?:boolean;
FollowDocument: FollowDocument;
} }

View File

@ -33,21 +33,10 @@ export class followDocumentPreview extends React.Component<IfollowDocumentPrevie
const graphService: Graph = new Graph(); const graphService: Graph = new Graph();
const initialized = await graphService.initialize(this.props.context.serviceScope); const initialized = await graphService.initialize(this.props.context.serviceScope);
if (initialized) { if (initialized) {
const HeaderItem = { let graphData: any = await graphService.postGraphContent(`https://graph.microsoft.com/v1.0/drives/${this.props.FollowDocument.DriveId}/items/${this.props.FollowDocument.ItemId}/preview`, {});
"requests": [
{
"entityTypes": ["driveItem"],
"query": {
"queryString": "path:\"" + this.props.url.replace(this.props.filename, "") + "\" Filename:\"" + this.props.filename + "\"",
},
},
],
};
const tmpFileID = await graphService.postGraphContent("https://graph.microsoft.com/beta/search/query", HeaderItem);
let graphData: any = await graphService.postGraphContent(`https://graph.microsoft.com/v1.0/drives/${tmpFileID.value[0].hitsContainers[0].hits[0].resource.parentReference.driveId}/items/${tmpFileID.value[0].hitsContainers[0].hits[0].resource.id}/preview`, {});
this.setState({ this.setState({
preview: graphData.getUrl, preview: graphData.getUrl,
name: tmpFileID.value[0].hitsContainers[0].hits[0].resource.name, name: this.props.FollowDocument.Title,
visible: true, visible: true,
}); });
} }

View File

@ -1,7 +1,8 @@
import { WebPartContext } from "@microsoft/sp-webpart-base"; import { WebPartContext } from "@microsoft/sp-webpart-base";
import { FollowDocument } from '../../models/followDocument';
export interface IfollowDocumentSendMessageProps { export interface IfollowDocumentSendMessageProps {
close: () => void; close: () => void;
url: string; url: string;
context: WebPartContext; context: WebPartContext;
fileInfo:any; fileInfo: FollowDocument;
} }

View File

@ -9,6 +9,7 @@ import { ITag, } from "office-ui-fabric-react/lib/Pickers";
import * as AdaptiveCards from "adaptivecards"; import * as AdaptiveCards from "adaptivecards";
import Graph from "../../Service/GraphService"; import Graph from "../../Service/GraphService";
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { FollowDocument } from '../../models/followDocument';
export class FollowDocumentSendMessage extends React.Component<IfollowDocumentSendMessageProps, IfollowDocumentSendMessageState> { export class FollowDocumentSendMessage extends React.Component<IfollowDocumentSendMessageProps, IfollowDocumentSendMessageState> {
private card: any; private card: any;
@ -47,7 +48,7 @@ export class FollowDocumentSendMessage extends React.Component<IfollowDocumentSe
const HeadersendMessage = { const HeadersendMessage = {
"body": { "body": {
"contentType": "html", "contentType": "html",
"content": this._acContainer.innerHTML + `<a href="${(this.props.fileInfo.fields.ServerUrlProgid === undefined ? this.props.fileInfo.fields.Url : this.props.fileInfo.fields.ServerUrlProgid.substring(1))}">${this.props.fileInfo.fields.Title}</a>` "content": this._acContainer.innerHTML + `<a href="${this.props.fileInfo.WebFileUrl}">${this.props.fileInfo.Title}</a>`
} }
}; };
const getresult = await graphService.postGraphContent(`https://graph.microsoft.com/v1.0/teams/${this.state.selectedTeamId}/channels/${this.state.selectedTeamChannelId}/messages`, HeadersendMessage); 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<IfollowDocumentSe
return getresult; return getresult;
} }
public showAdaptiveCard(fileInfo: any) { public showAdaptiveCard(fileInfo: FollowDocument) {
this.card = { this.card = {
"type": "AdaptiveCard", "type": "AdaptiveCard",
@ -86,7 +87,7 @@ export class FollowDocumentSendMessage extends React.Component<IfollowDocumentSe
"items": [ "items": [
{ {
"type": "Image", "type": "Image",
"url": fileInfo.fields.IconUrl, "url": fileInfo.IconUrl,
"size": "Small", "size": "Small",
"spacing": "Medium" "spacing": "Medium"
} }
@ -99,7 +100,7 @@ export class FollowDocumentSendMessage extends React.Component<IfollowDocumentSe
{ {
"type": "TextBlock", "type": "TextBlock",
"weight": "Bolder", "weight": "Bolder",
"text": fileInfo.fields.Title, "text": fileInfo.Title,
"wrap": true "wrap": true
} }
], ],

View File

@ -0,0 +1,23 @@
export interface FollowDocument {
id?: string;
Title?: string;
WebName?: string;
WebUrl?: string;
Domain?: string;
Folder?: string;
ItemProperties?: string;
WebFileUrl?: string;
DriveId?: string;
ListId?: string;
SiteId?: string;
ItemId?: string;
Url?: string;
DownloadFile?: string;
Thumbnail?: string;
IconUrl?: string;
documentCardActions?: Array<any>;
preview?: string;
Description?: string;
followedDateTime?: Date;
}