Merge pull request #1939 from aaclage/Feature/FollowDocumentWebPart

New Sample Follow Document WebPart
This commit is contained in:
Hugo Bernier 2021-06-27 01:49:42 -04:00 committed by GitHub
commit efb004c40a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 23844 additions and 0 deletions

View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.12.1",
"libraryName": "follow-document-web-part",
"libraryId": "fe2b78b4-7fce-42d8-93b9-a2a239770cbe",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

View File

@ -0,0 +1,50 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-follow-document",
"source": "pnp",
"title": "react-follow-document",
"shortDescription": "react-follow-document",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-follow-document",
"longDescription": [
"identify/follow user key documents from all Tenant and easily access them in Modern Pages and Microsoft Teams"
],
"created": "2021-06-21",
"modified": "2021-06-21",
"products": [
"SharePoint",
"Office"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.12.1"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/blob/main/samples/react-follow-document/assets/FollowDocumentSample1.gif?raw=true",
"alt": "react-follow-document"
}
],
"authors": [
{
"gitHubAccount": "aaclage",
"pictureUrl": "https://github.com/aaclage.png",
"name": "André Lage"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,124 @@
# Follow Document
## 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 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.
Available features:
- Display Follow Documents as Document Card Grid.
- Click on icon redirects to Document.
- Click on Site name redirects to Site.
- Team icon, Form to send message with html to Teams using adaptive cards.
- Folder Icon redirects to Library where document is located.
- Filled Start allow user to unfollow document.
- Info icon open the Properties of Document with capability to edit.
- Document with search icon allow to preview Document in Side Panel.
- Search by Filename.
- Grouping by Site.
- 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 [adaptive cards](https://adaptivecards.io/)
- Microsoft Teams integration with following option [TeamsTab, TeamsPersonalApp]
![image](./Assets/FollowDocumentSample1.gif)
## Compatibility
![SPFx 1.12.1](https://img.shields.io/badge/SPFx-1.12.1-green.svg)
![Node.js LTS v14 | LTS v12 | LTS v10](https://img.shields.io/badge/Node.js-LTS%20v14%20%7C%20LTS%20v12%20%7C%20LTS%20v10-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
![Teams Yes: Designed for Microsoft Teams](https://img.shields.io/badge/Teams-Yes-green.svg "Designed for Microsoft Teams")
![Workbench Local](https://img.shields.io/badge/Workbench-Local-yellow.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
### Grant the service principal permission to the Microsoft Graph API
Once installed, the solution will request the required permissions via the **Office 365 admin portal > SharePoint > Advanced > API access**.
If you prefer to approve the permissions in advance, for example when testing the solution in the Workbench page without installing it, you can do so using the [CLI for Microsoft 365](https://pnp.github.io/cli-microsoft365/):
```bash
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 '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'
```
## Solution
Solution|Author(s)
--------|---------
react-follow-document | [André Lage](https://github.com/aaclage) (http://aaclage.blogspot.com, [@aaclage](https://twitter.com/aaclage))
## Version history
Version|Date|Comments
-------|----|--------
1.0|June 22, 2021|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- **gulp serve**
- **gulp bundle --ship**
- **gulp package-solution --ship**
- Add to AppCatalog and deploy
## Features
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.
- 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).
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
## Help
We do not support samples, but we this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=bug-report.yml&sample=react-follow-document-WebPart&authors=@aaclage&title=react-follow-document-WebPart%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=question.yml&sample=react-follow-document-WebPart&authors=@aaclage&title=react-follow-document-WebPart%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=suggestion.yml&sample=react-follow-document-WebPart&authors=@aaclage&title=react-follow-document-WebPart%20-%20).
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-follow-document" />

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"follow-document-web-part-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/followDocumentWebPart/FollowDocumentWebPartWebPart.js",
"manifest": "./src/webparts/followDocumentWebPart/FollowDocumentWebPartWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"FollowDocumentWebPartWebPartStrings": "lib/webparts/followDocumentWebPart/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "./release/assets/"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "follow-document-web-part",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "follow-document-web-part-client-side-solution",
"id": "fe2b78b4-7fce-42d8-93b9-a2a239770cbe",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"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"
}]
},
"paths": {
"zippedPackage": "solution/follow-document-web-part.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://contoso.sharepoint.com/sites/Example/sitepages/home.aspx?debug=true&noredir=true&debugManifestsFile=https://localhost:4321/temp/manifests.js",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,16 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
{
"name": "follow-document-web-part",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.12.1",
"@microsoft/sp-dialog": "^1.12.1",
"@microsoft/sp-lodash-subset": "1.12.1",
"@microsoft/sp-office-ui-fabric-core": "1.12.1",
"@microsoft/sp-property-pane": "1.12.1",
"@microsoft/sp-webpart-base": "1.12.1",
"@pnp/spfx-controls-react": "^3.1.0",
"office-ui-fabric-react": "7.156.0",
"react": "16.9.0",
"react-adaptivecards": "^0.1.3",
"react-dom": "16.9.0"
},
"devDependencies": {
"@types/react": "16.9.36",
"@types/react-dom": "16.9.8",
"@microsoft/sp-build-web": "1.12.1",
"@microsoft/sp-tslint-rules": "1.12.1",
"@microsoft/sp-module-interfaces": "1.12.1",
"@microsoft/sp-webpart-workbench": "1.12.1",
"@microsoft/rush-stack-compiler-3.7": "0.2.3",
"gulp": "~4.0.2",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "e26d8b3f-340e-4814-b444-ad29e42cb7fd",
"alias": "FollowDocumentWebPartWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart", "TeamsTab","TeamsPersonalApp"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Follow Document" },
"description": { "default": "followDocumentWebPart description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "followDocumentWebPart"
}
}]
}

View File

@ -0,0 +1,68 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import { initializeIcons } from "office-ui-fabric-react";
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import * as strings from "FollowDocumentWebPartWebPartStrings";
import FollowDocumentWebPart from "./components/FollowDocumentWebPart";
import { IFollowDocumentWebPartProps } from "./components/IFollowDocumentWebPartProps";
export interface IFollowDocumentWebPartWebPartProps {
Title: string;
}
export default class FollowDocumentWebPartWebPart extends BaseClientSideWebPart<IFollowDocumentWebPartWebPartProps> {
protected onInit() {
if (this.context.sdks.microsoftTeams) initializeIcons();
return super.onInit();
}
public render(): void {
const element: React.ReactElement<IFollowDocumentWebPartProps> =
React.createElement(FollowDocumentWebPart, {
title: this.properties.Title,
context: this.context,
displayMode: this.displayMode,
updateProperty: (value: string) => {
this.properties.Title = value;
},
});
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription,
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("Title", {
label: strings.TitleFieldLabel,
value: strings.TitleFieldValue,
}),
],
},
],
},
],
};
}
}

View File

@ -0,0 +1,45 @@
import { AadHttpClient, MSGraphClientFactory, MSGraphClient } from "@microsoft/sp-http";
import { WebPartContext } from "@microsoft/sp-webpart-base";
export default class Graph {
private client: MSGraphClient;
public async initialize(serviceScope): Promise<boolean> {
const graphFactory: MSGraphClientFactory = serviceScope.consume(
MSGraphClientFactory.serviceKey
);
return graphFactory.getClient().then((client) => {
this.client = client;
return true;
});
}
public async getGraphContent(graphQuery: string, context: WebPartContext) {
// Using Graph here, but any 1st or 3rd party REST API that requires Azure AD auth can be used here.
return new Promise<any>((resolve, reject) => {
context.aadHttpClientFactory
.getClient("https://graph.microsoft.com")
.then((client: AadHttpClient) => {
// Querys to Graph base on url
return client.get(`${graphQuery}`, AadHttpClient.configurations.v1);
})
.then((response) => {
return response.json();
})
.then((json) => {
resolve(json);
})
.catch((error) => {
console.error(error);
reject(error);
});
});
}
public async postGraphContent(graphQuery: string,Header) {
// Using Graph here, but any 1st or 3rd party REST API that requires Azure AD auth can be used here.
const saveResult = await this.client
.api(graphQuery)
.post(JSON.stringify(Header));
return saveResult;
}
}

View File

@ -0,0 +1,146 @@
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

@ -0,0 +1,39 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
.grid {
color: inherit;
}
.documentTile {
background-color: transparent;
outline: transparent;
position: relative;
}
.gridItem {
color: inherit;
//margin-bottom: 20px;
}
.documentTile:hover::after {
content: " ";
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
pointer-events: none;
border-width: 1px;
border-style: solid;
border-image: initial;
border-color: $ms-color-themeDarker !important;
}
.spinnerLoading{
padding-top:10px;
padding-bottom:10px;
}
.DocumentCardActionsPadding{
padding: 4px 4px;
}

View File

@ -0,0 +1,442 @@
import * as React from 'react';
import * as ReactDom from "react-dom";
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";
// Used to render list grid
import {
DocumentCard,
DocumentCardDetails,
DocumentCardActions,
DocumentCardTitle,
DocumentCardLocation,
DocumentCardType,
DocumentCardImage
} from 'office-ui-fabric-react/lib/DocumentCard';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { followDocumentPreview } from './followDocumentPreview/followDocumentPreview';
import { IfollowDocumentPreviewProps } from './followDocumentPreview/IfollowDocumentPreviewProps';
import FollowDocumentDialog from './followDocumentDialog/followDocumentDialog';
import { followType } from '../util/followType';
import { ImageFit } from 'office-ui-fabric-react/lib/Image';
import { ISize } from 'office-ui-fabric-react/lib/Utilities';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
import { Stack, IStackTokens } from 'office-ui-fabric-react/lib/Stack';
const stackTokens: Partial<IStackTokens> = { childrenGap: 20 };
export default class FollowDocumentWebPart extends React.Component<IFollowDocumentWebPartProps, IFollowDocumentWebPartState> {
private _siteId: string = null;
private _listId: string = null;
private _panelPlaceHolder: HTMLDivElement = null;
private _selectedGroup: string = "0";
constructor(props) {
super(props);
this.state = {
Items: [],
siteId: null,
listId: null,
previewImgUrl: null,
visible: true,
};
this._panelPlaceHolder = document.body.appendChild(
document.createElement("div")
);
this.getListItems();
}
private getListItems = () => {
this._selectedGroup = "0";
if (!this.state.visible) {
this.setState({
visible: true,
});
}
//Load using Graph
this.getGraphFollowedDocs();
}
//get Web Name and Web Url of Document
private getSearchWebID = async (graphData: any[], webs: any[]): Promise<any[]> => {
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: any, ev: React.SyntheticEvent<HTMLElement>): void => {
const dialog: FollowDocumentDialog = new FollowDocumentDialog();
dialog.initializedTeams(action, this.props.context, followType.SendTeams);
ev.stopPropagation();
ev.preventDefault();
}
private getSearchListItemID = async (ListId: string): Promise<string> => {
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({
context: this.props.context,
url: Url,
filename: Title,
isOpen: true,
});
}
private _renderPanelComponent = (props: IfollowDocumentPreviewProps): void => {
const element: React.ReactElement<IfollowDocumentPreviewProps> =
React.createElement(followDocumentPreview, props);
ReactDom.render(element, this._panelPlaceHolder);
}
private onActionPropertiesClick = (action: any, ev: React.SyntheticEvent<HTMLElement>): void => {
//Get Document Display Form List
this.getListItemID(action.fields.ListId.replace('{', '').replace('}', ''), action.fields.ItemId);
ev.stopPropagation();
ev.preventDefault();
}
private onActionFolderClick = (action: any, ev: React.SyntheticEvent<HTMLElement>): void => {
window.open(action.fields.Url.replace(action.fields.Title, ""), "_blank");
ev.stopPropagation();
ev.preventDefault();
}
/**
* Unfollow Option
*/
private onActionUnfollowClick = async (action: any, ev: React.SyntheticEvent<HTMLElement>) => {
ev.stopPropagation();
ev.preventDefault();
const dialog: FollowDocumentDialog = new FollowDocumentDialog();
dialog._followTypeDialog = followType.Unfollow;
dialog._filename = action.fields.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();
}
}
});
}
private onActionPanelClick = async (action: any, ev: React.SyntheticEvent<HTMLElement>) => {
this._showPanel(action.fields.Url, action.fields.Title);
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<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> {
//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));
} else {
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,
});
};
const checkClear = (ev: any) => {
let items = [];
if (this._selectedGroup === "0") {
items = this.state.Items;
} else {
items = this.state.Items.filter(item => (item.webUrl.toLowerCase().indexOf(this._selectedGroup.toLowerCase()) > -1));
}
this.setState({
ItemsSearch: items,
});
};
const filterall = (event: React.FormEvent<HTMLDivElement>, selectedOption: IDropdownOption) => {
this._selectedGroup = selectedOption.key.toString();
if (selectedOption.key.toString() === "0") {
this.setState({
ItemsSearch: this.state.Items,
});
} else {
const items = this.state.Items.filter(item => item.webUrl.toLowerCase().indexOf(selectedOption.key.toString().toLowerCase()) > -1);
this.setState({
ItemsSearch: items,
});
}
};
return (
<>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty} moreLink={
<div style={{ display: "inline-flex" }}>
{(!this.state.visible) &&
<div>
<IconButton
iconProps={{ iconName: 'Refresh' }}
onClick={
this.getListItems
} allowDisabledFocus disabled={false} checked={false}
/>
</div>
}
{(!this.state.visible) &&
<Dropdown
placeholder="Filter by Site"
onChange={filterall}
tabIndex={0}
// eslint-disable-next-line react/jsx-no-bind
options={this.state.ItemsGroup}
styles={{ dropdown: { width: 300 } }}
/>
}
</div>
} />
<div className={styles.spinnerLoading}>
{(this.state.visible) &&
<Spinner size={SpinnerSize.large} />
}
{(!this.state.visible) &&
<Stack tokens={stackTokens}>
<SearchBox style={{ width: "80%" }} placeholder="Search Document" onSearch={checkSearchDrive} onClear={checkClear} />
</Stack>
}
</div>
<div className={styles.grid}>
<FollowDocumentGrid
items={this.state.ItemsSearch}
onRenderGridItem={(item: any, finalSize: ISize, isCompact: boolean) => this._onRenderGridItem(item, finalSize, isCompact)}
/>
</div>
</>
);
}
private _onRenderGridItem = (item: any, finalSize: ISize, isCompact: boolean): JSX.Element => {
return <div
className={styles.documentTile}
data-is-focusable={true}
role="listitem"
aria-label={item.title}
>
<DocumentCard
type={isCompact ? DocumentCardType.compact : DocumentCardType.normal}
>
<div style={{ cursor: 'pointer' }} onClick={() => window.open(item.url, '_blank')}>
<DocumentCardImage height={100} imageFit={ImageFit.center} imageSrc={item.profileImageSrc} />
</div>
{!isCompact && <DocumentCardLocation location={item.webName} onClick={() => window.open(item.webUrl, '_blank')} />}
<DocumentCardDetails>
<DocumentCardTitle
title={item.title}
shouldTruncate={true}
/>
<DocumentCardActions className={styles.DocumentCardActionsPadding} actions={item.documentCardActions} />
</DocumentCardDetails>
</DocumentCard>
</div>;
}
}

View File

@ -0,0 +1,16 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from '@microsoft/sp-core-library';
export interface IFollowDocumentWebPartProps {
title: string;
context: WebPartContext;
displayMode: DisplayMode;
updateProperty: (value: string) => void;
}
export interface IGridItem {
thumbnail: string;
title: string;
name: string;
profileImageSrc: string;
location: string;
activity: string;
}

View File

@ -0,0 +1,10 @@
import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
export interface IFollowDocumentWebPartState {
siteId?: string;
listId?: string;
Items: any;
ItemsSearch?: any;
ItemsGroup?: IDropdownOption[];
previewImgUrl:string;
visible: boolean;
}

View File

@ -0,0 +1,92 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { followType } from "../../util/followType";
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
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';
export default class FollowDocumentDialog extends BaseDialog {
public _followDocumentState: boolean = false;
private _webUrl: string;
public _filename:string;
private _context:WebPartContext;
public _followTypeDialog: followType;
public _fileInfo: any;
public return: (string) => void;
public async initialize(url: string, type: followType = followType.Blank) {
this._webUrl = url;
this._followTypeDialog = type;
this.show();
}
public async initializedTeams(fileInfo: any,context:WebPartContext, type: followType) {
this._context = context;
this._fileInfo = fileInfo;
this._followTypeDialog = type;
this.show();
}
public render(): void {
let reactElement;
const Unfollow = () => {
this._followDocumentState = true;
this.close();
};
switch (this._followTypeDialog) {
case followType.ViewPropreties:
reactElement =
<FollowDocumentProperties
url={this._webUrl}
close={this.close}
/>;
break;
case followType.SendTeams:
reactElement =
<FollowDocumentSendMessage
url={this._webUrl}
close={this.close}
context={this._context}
fileInfo={this._fileInfo}
/>;
break;
case followType.Unfollow:
reactElement = <DialogContent
title="Follow Status"
showCloseButton={true}
onDismiss={this.close}
>
<div>
<div>Do you want to unfollow <b>"{this._filename}"</b>?</div>
<DialogFooter>
<PrimaryButton onClick={Unfollow} text="Unfollow" />
<DefaultButton onClick={this.close} text="Cancel" />
</DialogFooter>
</div>
</DialogContent>;
break;
default:
throw new Error("Unknown command");
}
ReactDOM.render(reactElement, this.domElement);
}
protected onAfterClose(): void {
super.onAfterClose();
// Clean up the element for the next dialog
ReactDOM.unmountComponentAtNode(this.domElement);
}
public getConfig(): IDialogConfiguration {
return {
isBlocking: false
};
}
}

View File

@ -0,0 +1,56 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
:export {
padding: 10;
minWidth: 200;
maxWidth: 200;
compactThreshold: 480;
rowsPerPage: 100;
}
.followDocumentGrid {
overflow: hidden;
font-size: 0;
position: relative;
background-color: transparent;
:global(.ms-DocumentCard) {
position: relative;
background-color: $ms-color-white;
height: 100%;
&:global(.ms-DocumentCard--compact) {
:global(.ms-DocumentCardImage) {
-ms-flex-negative: 0;
flex-shrink: 0;
width: 144px;
}
}
:global(.ms-DocumentCardImage-icon) img {
width: 32px;
height: 32px;
}
}
:global(.ms-DocumentCard:not(.ms-DocumentCard--compact)) {
min-width: 200px;
max-width: 200px;
:global(.ms-DocumentCardTile-titleArea) {
height: 81px;
}
:global(.ms-DocumentCardLocation) {
padding: 12px 16px 5px 16px;
overflow: hidden;
text-overflow: ellipsis;
}
}
:global(.ms-List-cell) {
vertical-align: top;
display: inline-block;
margin-bottom: 20px;
}
}

View File

@ -0,0 +1,59 @@
import * as React from 'react';
import styles from './followDocumentGrid.module.scss';
// Used to render list grid
import { FocusZone } from 'office-ui-fabric-react/lib/FocusZone';
import { List } from 'office-ui-fabric-react/lib/List';
import { IRectangle, ISize } from 'office-ui-fabric-react/lib/Utilities';
import { IfollowDocumentGridProps, IfollowDocumentGridState } from './followDocumentGrid.types';
const ROWS_PER_PAGE: number = +styles.rowsPerPage;
export class FollowDocumentGrid extends React.Component<IfollowDocumentGridProps, IfollowDocumentGridState> {
private _columnWidth: number;
private _rowHeight: number;
private _isCompact: boolean;
public render(): React.ReactElement<IfollowDocumentGridProps> {
return (
<div role="group" aria-label={this.props.ariaLabel}>
<FocusZone>
<List
role="presentation"
className={styles.followDocumentGrid}
items={this.props.items}
getItemCountForPage={this._getItemCountForPage}
getPageHeight={this._getPageHeight}
onRenderCell={this._onRenderCell}
{...this.props.listProps}
/>
</FocusZone>
</div>
);
}
private _getItemCountForPage = (itemIndex: number, surfaceRect: IRectangle): number => {
return ROWS_PER_PAGE;
}
private _getPageHeight = (): number => {
return this._rowHeight * ROWS_PER_PAGE;
}
private _onRenderCell = (item: any, index: number | undefined): JSX.Element => {
const isCompact: boolean = this._isCompact;
const finalSize: ISize = { width: this._columnWidth, height: this._rowHeight };
return (
<div
style={{
width: "200px",
marginRight: "20px"
}}
>
{this.props.onRenderGridItem(item, finalSize, isCompact)}
</div>
);
}
}

View File

@ -0,0 +1,12 @@
import { ISize } from 'office-ui-fabric-react/lib/Utilities';
import { IListProps } from 'office-ui-fabric-react/lib/List';
export interface IfollowDocumentGridProps {
ariaLabel?: string;
items: any[];
listProps?: Partial<IListProps>;
onRenderGridItem: (item: any, finalSize: ISize, isCompact: boolean) => JSX.Element;
}
export interface IfollowDocumentGridState {}

View File

@ -0,0 +1,2 @@
export * from './followDocumentGrid.types';
export * from './followDocumentGrid';

View File

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

View File

@ -0,0 +1,8 @@
import * as MicrosoftGraph from "@microsoft/microsoft-graph-types";
export interface IfollowDocumentPreviewState {
isOpen: boolean;
preview?: string;
name?: string;
visible?: boolean;
}

View File

@ -0,0 +1,12 @@
.panelTitle {
font-size: 21px;
font-weight: 100;
}
.footerSection {
& button,
& a {
margin-right: 20px;
}
}

View File

@ -0,0 +1,97 @@
import * as React from 'react';
import styles from './followDocumentPreview.module.scss';
import { Panel, PanelType } from "office-ui-fabric-react/lib/Panel";
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { DefaultButton } from "office-ui-fabric-react/lib/Button";
import Graph from "../../Service/GraphService";
import { IfollowDocumentPreviewProps } from './IfollowDocumentPreviewProps';
import { IfollowDocumentPreviewState } from './IfollowDocumentPreviewState';
export class followDocumentPreview extends React.Component<IfollowDocumentPreviewProps, IfollowDocumentPreviewState> {
constructor(props: IfollowDocumentPreviewProps) {
super(props);
this.state = {
isOpen: true,
visible: false,
};
this.getSearchItemID();
}
public async componentWillReceiveProps(nextProps: IfollowDocumentPreviewProps): Promise<void> {
// open panel
this.setState({
isOpen: nextProps.isOpen,
visible: false,
});
this.getSearchItemID();
}
private getSearchItemID = async () => {
const graphService: Graph = new Graph();
const initialized = await graphService.initialize(this.props.context.serviceScope);
if (initialized) {
const HeaderItem = {
"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({
preview: graphData.getUrl,
name: tmpFileID.value[0].hitsContainers[0].hits[0].resource.name,
visible: true,
});
}
}
public render(): React.ReactElement<IfollowDocumentPreviewProps> {
return (
<Panel isOpen={this.state.isOpen}
type={PanelType.large}
isLightDismiss
headerText={this.state.name}
onRenderFooterContent={this._onRenderFooterContent}
onDismiss={this._closePanel}
>
<div>
<label>Preview Document</label>
{(!this.state.visible) &&
<Spinner size={SpinnerSize.large} />
}
<iframe style={{ width: "100%", height: "800px" }} src={this.state.preview} frameBorder={0}></iframe>
</div>
</Panel>
);
}
private _onRenderFooterContent = () => {
return (
<div className={styles.footerSection}>
<DefaultButton text="Cancel" onClick={this._closePanel} />
</div>
);
}
/**
* Close extension panel
*/
private _closePanel = () => {
this.setState({
isOpen: false,
visible: true,
preview: ""
});
}
}

View File

@ -0,0 +1,5 @@
export interface IfollowDocumentPropertiesProps {
close: () => void;
url: string;
iframeOnLoad?: (iframe: any) => void;
}

View File

@ -0,0 +1,40 @@
import * as React from 'react';
import { DialogContent } from 'office-ui-fabric-react/lib/Dialog';
import { IfollowDocumentPropertiesProps } from "./IfollowDocumentPropertiesProps";
export class FollowDocumentProperties extends React.Component<IfollowDocumentPropertiesProps> {
private _iframe: any;
constructor(props) {
super(props);
}
public render(): React.ReactElement<IfollowDocumentPropertiesProps> {
return (
<DialogContent
title="Follow Status"
showCloseButton={true}
onDismiss={this.props.close}
>
<div>
<iframe ref={(iframe) => { this._iframe = iframe; }} onLoad={this._iframeOnLoad.bind(this)}
style={{ width: "600px", height: "800px" }} src={this.props.url} frameBorder={0}></iframe>
</div>
</DialogContent>
);
}
private _iframeOnLoad(): void {
try {
this._iframe.contentWindow.frameElement.cancelPopUp = this.props.close;
} catch (err) {
if (err.name !== "SecurityError") {
throw err;
}
}
if (this.props.iframeOnLoad) {
this.props.iframeOnLoad(this._iframe);
}
}
}

View File

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

View File

@ -0,0 +1,7 @@
import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
export interface IfollowDocumentSendMessageState {
selectedTeamOption: IDropdownOption[];
selectedTeamChannelsOption: IDropdownOption[];
selectedTeamId: string;
selectedTeamChannelId: string;
}

View File

@ -0,0 +1,206 @@
import * as React from 'react';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { DialogContent, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { IfollowDocumentSendMessageProps } from "./IfollowDocumentSendMessageProps";
import { IfollowDocumentSendMessageState } from "./IfollowDocumentSendMessageState";
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';
export class FollowDocumentSendMessage extends React.Component<IfollowDocumentSendMessageProps, IfollowDocumentSendMessageState> {
private card: any;
private _acContainer: HTMLDivElement;
constructor(props) {
super(props);
this.state = {
selectedTeamChannelsOption: [],
selectedTeamOption: [],
selectedTeamId: "",
selectedTeamChannelId: "",
};
this.loadTeams();
}
public componentDidMount(): void {
this.showAdaptiveCard(this.props.fileInfo);
}
public loadTeams = async () => {
const Teams = await this.getjoinedTeams();
let Teamsoptions: Array<IDropdownOption> = new Array<IDropdownOption>();
Teams.value.forEach(element => {
Teamsoptions.push({
key: element.id,
text: element.displayName,
});
});
this.setState({
selectedTeamOption: Teamsoptions,
});
}
private sendMessageChannell = async () => {
const graphService: Graph = new Graph();
const initialized = await graphService.initialize(this.props.context.serviceScope);
if (initialized) {
const HeadersendMessage = {
"body": {
"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>`
}
};
const getresult = await graphService.postGraphContent(`https://graph.microsoft.com/v1.0/teams/${this.state.selectedTeamId}/channels/${this.state.selectedTeamChannelId}/messages`, HeadersendMessage);
}
}
private getjoinedTeams = async () => {
const graphService: Graph = new Graph();
const getresult = await graphService.getGraphContent(`https://graph.microsoft.com/v1.0/me/joinedTeams?$select=id,displayName`, this.props.context);
return getresult;
}
private getTeamsChannels = async (TeamID) => {
const graphService: Graph = new Graph();
const getresult = await graphService.getGraphContent(`https://graph.microsoft.com/v1.0/teams/${TeamID}/channels?$select=id,displayName`, this.props.context);
return getresult;
}
public showAdaptiveCard(fileInfo: any) {
this.card = {
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"text": "Site: " + fileInfo.WebName
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "Image",
"url": fileInfo.fields.IconUrl,
"size": "Small",
"spacing": "Medium"
}
],
"width": "auto"
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": fileInfo.fields.Title,
"wrap": true
}
],
"width": "stretch"
}
]
},
{
"type": "TextBlock",
"text": "Comments: " + (fileInfo.Description === undefined ? "" : fileInfo.Description),
"wrap": true
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.2"
};
// Create an AdaptiveCard instance
var adaptiveCard = new AdaptiveCards.AdaptiveCard();
adaptiveCard.hostConfig = new AdaptiveCards.HostConfig({
fontFamily: "Segoe UI, Helvetica Neue, sans-serif"
});
// Parse the card
adaptiveCard.parse(this.card);
// Empty the div so we can replace it
while (this._acContainer.firstChild) {
this._acContainer.removeChild(this._acContainer.lastChild);
}
// Render the card to an HTML element
adaptiveCard.render(this._acContainer);
}
public render(): React.ReactElement<IfollowDocumentSendMessageProps> {
const sendMessageTeam = () => {
this.sendMessageChannell();
this.props.close();
};
const handleChange = (event) => {
this.props.fileInfo.Description = (event.target as HTMLInputElement).value;
this.showAdaptiveCard(this.props.fileInfo);
};
const handleSelectTeam = async (event: React.FormEvent<HTMLDivElement>, selectedOption: IDropdownOption) => {
this.setState({
selectedTeamId: "",
selectedTeamChannelsOption: null,
});
const TeamsChannels = await this.getTeamsChannels(selectedOption.key.toString());
let Teamsoptions: Array<IDropdownOption> = new Array<IDropdownOption>();
TeamsChannels.value.forEach(element => {
Teamsoptions.push({
key: element.id,
text: element.displayName,
});
});
this.setState({
selectedTeamId: selectedOption.key.toString(),
selectedTeamChannelsOption: Teamsoptions,
});
};
const handleSelectChannel = async (event: React.FormEvent<HTMLDivElement>, selectedOption: IDropdownOption) => {
this.setState({
selectedTeamChannelId: selectedOption.key.toString(),
});
};
return (
<DialogContent
title="Send Document Card"
showCloseButton={true}
onDismiss={this.props.close}
>
<div>
<Dropdown
label="My Teams"
options={this.state.selectedTeamOption}
styles={{ dropdown: { width: 300 } }}
onChange={handleSelectTeam}
/>
{this.state?.selectedTeamId !== "" && (
<>
<Dropdown
label="Teams Channels"
options={this.state.selectedTeamChannelsOption}
styles={{ dropdown: { width: 300 } }}
onChange={handleSelectChannel}
notifyOnReselect={true}
/>
</>
)}
<TextField onChange={handleChange} label="Comments" multiline rows={3} />
<div ref={(elm) => { this._acContainer = elm; }}></div>
</div>
<DialogFooter>
<PrimaryButton onClick={sendMessageTeam} text="Send" />
<DefaultButton onClick={this.props.close} text="Cancel" />
</DialogFooter>
</DialogContent>
);
}
}

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"PropertyPaneDescription": "Follow Document configurable properties.",
"BasicGroupName": "Properties",
"TitleFieldLabel": "Web Part Title",
"TitleFieldValue": "Followed Documents",
}
});

View File

@ -0,0 +1,11 @@
declare interface IFollowDocumentWebPartWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
TitleFieldLabel: string;
TitleFieldValue: string;
}
declare module 'FollowDocumentWebPartWebPartStrings' {
const strings: IFollowDocumentWebPartWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,6 @@
export enum followType {
Blank = 0,
ViewPropreties = 1,
Unfollow = 2,
SendTeams = 3,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.7/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}