Initial commit (#1130)
Co-authored-by: Mikael Svenson <miksvenson@gmail.com> Co-authored-by: Laura Kokkarinen <41330990+LauraKokkarinen@users.noreply.github.com>
This commit is contained in:
parent
dc6bfe633b
commit
cead35f5db
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
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
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"plusBeta": true,
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.10.0",
|
||||
"libraryName": "outlook-2-sp-spfx",
|
||||
"libraryId": "41e21307-ed8c-4409-b12f-9c575675bb37",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
## outlook-2-teams-spfx
|
||||
|
||||
|
||||
## Summary
|
||||
This SPFx Outlook Add-In lets you browse your OneDrive, joined Teams or Groups and select a folder to save your complete mail in there.
|
||||
This sample shows you working with the current Office context and receive information on currently selected mail from there.
|
||||
Furthermore it shows you how to retrieve a complete mail as a mimestream via Microsoft Graph and finally two file operations with Microsoft Graph as well:
|
||||
* Writing normal files smaller 4MB
|
||||
* Writing big files with an UploadSession when bigger than 4MB
|
||||
|
||||
## outlook-2-teams-spfx in action
|
||||
![WebPartInAction](https://mmsharepoint.files.wordpress.com/2020/01/addin_overall.png)
|
||||
|
||||
A detailed functionality and technical description can be found in the [author's blog series](https://mmsharepoint.wordpress.com/2020/01/11/an-outlook-add-in-with-sharepoint-framework-spfx-introduction/)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![drop](https://img.shields.io/badge/drop-1.10.0-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [Tutorial for creating Outlook Web Access extension using SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/office-addins-tutorial)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
outlook-2-teams-spfx| Markus Moeller ([@moeller2_0](http://www.twitter.com/moeller2_0))
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|February 05, 2020|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
|
||||
* in the command line run:
|
||||
* restore dependencies: `npm install`
|
||||
From here you can also follow the deployment steps from the official [Microsoft Tutorial](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/office-addins-tutorial#packaging-and-deploying-your-solution-to-sharepoint)
|
||||
* build solution `gulp build --ship`
|
||||
* bundle solution: `gulp bundle --ship`
|
||||
* package solution: `gulp package-solution --ship`
|
||||
* locate solution at `.\sharepoint\solution\outlook-2-sp-spfx.sppkg`
|
||||
* upload it to your tenant app catalog
|
||||
* Go to your Outlook Web Access and selct a mail
|
||||
* Choose ... and "Get Add Ins"
|
||||
* Choose My Add-ins from left menu
|
||||
* Choose *Add from file... under the Custom add-ins
|
||||
* Upload the manifest xml file from \officeAddin folder
|
||||
* Click Install on the warning message to get your add-in available on the tenant
|
||||
* Close the add-in window by clicking X on the top-right corner
|
||||
* Activate again the context menu from [...] and choose "Copy to SharePoint" to activate the add-in in your inbox
|
||||
|
||||
## Features
|
||||
|
||||
This Outlook Add-In shows the following capabilities on top of the SharePoint Framework:
|
||||
|
||||
* Select Office context and attributes of currently selected mail
|
||||
* Use Microsoft Graph to retrieve joined Groups and Teams
|
||||
* Use Microsoft Graph to retrieve folders and subfolders for OneDrive or Teams/Group drives
|
||||
* Use Microsoft Graph to retrieve complete mail mimestream by given ID
|
||||
* Use Microsoft Graph to save normal or big files (in size bigger 4MB) with different concepts
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-outlook-copy2teams" />
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"outlook-2-share-point-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/outlook2SharePoint/Outlook2SharePointWebPart.js",
|
||||
"manifest": "./src/webparts/outlook2SharePoint/Outlook2SharePointWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"Outlook2SharePointWebPartStrings": "lib/webparts/outlook2SharePoint/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "outlook-2-sp-spfx",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "outlook-2-sp-spfx-client-side-solution",
|
||||
"id": "41e21307-ed8c-4409-b12f-9c575675bb37",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Group.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Windows Azure Active Directory",
|
||||
"scope": "User.Read"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Files.Read"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Files.ReadWrite"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Mail.Read"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Sites.ReadWrite.All"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/outlook-2-sp-spfx.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
'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.`);
|
||||
|
||||
build.initialize(require('gulp'));
|
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
|
||||
xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0"
|
||||
xsi:type="MailApp">
|
||||
<Id>7f1ef545-1d02-4cbd-b4e1-2f4140c1667a</Id>
|
||||
<Version>1.0.0.0</Version>
|
||||
<ProviderName>SPFx Provider</ProviderName>
|
||||
<DefaultLocale>en-US</DefaultLocale>
|
||||
<DisplayName DefaultValue="Copy to SharePoint"/>
|
||||
<Description DefaultValue="An Add-In to copy full mails to Teams, Groups or OneDrive."/>
|
||||
<IconUrl DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
|
||||
<HighResolutionIconUrl DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
|
||||
<SupportUrl DefaultValue="https://localhost:4321/help"/>
|
||||
<AppDomains>
|
||||
<AppDomain>https://login.microsoftonline.com</AppDomain>
|
||||
<AppDomain>https://login.windows.net</AppDomain>
|
||||
</AppDomains>
|
||||
<Hosts>
|
||||
<Host Name="Mailbox" />
|
||||
</Hosts>
|
||||
<Requirements>
|
||||
<Sets>
|
||||
<Set Name="Mailbox" MinVersion="1.4" />
|
||||
<Set Name="SharePointHostedAddin" MinVersion="1.1" />
|
||||
</Sets>
|
||||
</Requirements>
|
||||
<FormSettings>
|
||||
<Form xsi:type="ItemRead">
|
||||
<DesktopSettings>
|
||||
<SourceLocation DefaultValue="https://_SharePointTenantUrl_/_layouts/15/outlookhostedapp.aspx?componentId=7f1ef545-1d02-4cbd-b4e1-2f4140c1667a&isConfigureMode=true"/>
|
||||
<RequestedHeight>250</RequestedHeight>
|
||||
</DesktopSettings>
|
||||
</Form>
|
||||
</FormSettings>
|
||||
<Permissions>ReadWriteMailbox</Permissions>
|
||||
<Rule xsi:type="RuleCollection" Mode="Or">
|
||||
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Read" />
|
||||
</Rule>
|
||||
<DisableEntityHighlighting>false</DisableEntityHighlighting>
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides" xsi:type="VersionOverridesV1_0">
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1" xsi:type="VersionOverridesV1_1">
|
||||
<Hosts>
|
||||
<Host xsi:type="MailHost">
|
||||
<DesktopFormFactor>
|
||||
<ExtensionPoint xsi:type="MessageReadCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="msgReadGroup">
|
||||
<Label resid="GroupLabel" />
|
||||
<Control xsi:type="Button" id="msgReadOpenPaneButton">
|
||||
<Label resid="TaskpaneButton.Label" />
|
||||
<Supertip>
|
||||
<Title resid="TaskpaneButton.Label" />
|
||||
<Description resid="TaskpaneButton.Tooltip" />
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16" />
|
||||
<bt:Image size="32" resid="Icon.32x32" />
|
||||
<bt:Image size="80" resid="Icon.80x80" />
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url" />
|
||||
</Action>
|
||||
</Control>
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
</DesktopFormFactor>
|
||||
</Host>
|
||||
</Hosts>
|
||||
<Resources>
|
||||
<bt:Images>
|
||||
<bt:Image id="Icon.16x16" DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
|
||||
<bt:Image id="Icon.32x32" DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
|
||||
<bt:Image id="Icon.80x80" DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
|
||||
</bt:Images>
|
||||
<bt:Urls>
|
||||
<bt:Url id="Taskpane.Url" DefaultValue="https://_SharePointTenantUrl_/_layouts/15/outlookhostedapp.aspx?componentId=7f1ef545-1d02-4cbd-b4e1-2f4140c1667a&isConfigureMode=true" />
|
||||
</bt:Urls>
|
||||
<bt:ShortStrings>
|
||||
<bt:String id="GroupLabel" DefaultValue="Add-in groupLabel"/>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Show Taskpane"/>
|
||||
</bt:ShortStrings>
|
||||
<bt:LongStrings>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Opens taskpane."/>
|
||||
</bt:LongStrings>
|
||||
</Resources>
|
||||
</VersionOverrides>
|
||||
</VersionOverrides>
|
||||
</OfficeApp>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "outlook-2-sp-spfx",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.10.0-plusbeta",
|
||||
"@microsoft/sp-lodash-subset": "1.10.0-plusbeta",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.10.0-plusbeta",
|
||||
"@microsoft/sp-property-pane": "1.10.0-plusbeta",
|
||||
"@microsoft/sp-webpart-base": "1.10.0-plusbeta",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/office-js": "^1.0.59",
|
||||
"@types/react": "16.8.8",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"office-ui-fabric-react": "6.189.2",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.10.0-plusbeta",
|
||||
"@microsoft/sp-tslint-rules": "1.10.0-plusbeta",
|
||||
"@microsoft/sp-module-interfaces": "1.10.0-plusbeta",
|
||||
"@microsoft/sp-webpart-workbench": "1.10.0-plusbeta",
|
||||
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
import { MSGraphClient, MSGraphClientFactory } from '@microsoft/sp-http';
|
||||
import Utilities from './Utilities';
|
||||
import { IFolder } from '../model/IFolder';
|
||||
import { IMail } from '../model/IMail';
|
||||
|
||||
export default class GraphController {
|
||||
private client: MSGraphClient;
|
||||
|
||||
constructor (graphFactory: MSGraphClientFactory, callback: () => void) {
|
||||
graphFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
this.client = client;
|
||||
callback();
|
||||
});
|
||||
|
||||
this.retrieveMimeMail = this.retrieveMimeMail.bind(this);
|
||||
}
|
||||
|
||||
public getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function retrieves all 1st-level folders from user's OneDrive
|
||||
*/
|
||||
public getOneDriveFolder(): Promise<IFolder[]> {
|
||||
return this.client
|
||||
.api('me/drive/root/children')
|
||||
.version('v1.0')
|
||||
.filter('folder ne null')
|
||||
.get()
|
||||
.then((response): any => {
|
||||
let folders: Array<IFolder> = new Array<IFolder>();
|
||||
response.value.forEach((item) => {
|
||||
folders.push({ id: item.id, name: item.name, driveID: item.parentReference.driveId, parentFolder: null});
|
||||
});
|
||||
return folders;
|
||||
});
|
||||
}
|
||||
|
||||
public getGroupRootFolders(group: IFolder): Promise<IFolder[]> {
|
||||
return this.client
|
||||
.api(`drives/${group.driveID}/root/children`)
|
||||
.version('v1.0')
|
||||
.filter('folder ne null')
|
||||
.get()
|
||||
.then((response): any => {
|
||||
let folders: Array<IFolder> = new Array<IFolder>();
|
||||
response.value.forEach((item) => {
|
||||
folders.push({ id: item.id, name: item.name, driveID: group.driveID, parentFolder: group});
|
||||
});
|
||||
return folders;
|
||||
});
|
||||
}
|
||||
|
||||
public getSubFolder(folder: IFolder): Promise<IFolder[]> {
|
||||
return this.client
|
||||
.api(`drives/${folder.driveID}/items/${folder.id}/children`)
|
||||
.version('v1.0')
|
||||
.filter('folder ne null')
|
||||
.get()
|
||||
.then((response): any => {
|
||||
let folders: Array<IFolder> = new Array<IFolder>();
|
||||
response.value.forEach((item) => {
|
||||
folders.push({ id: item.id, name: item.name, driveID: folder.driveID, parentFolder: folder});
|
||||
});
|
||||
return folders;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function retrievs the user's membership groups from Graph
|
||||
*/
|
||||
public getJoinedGroups(): Promise<IFolder[]> {
|
||||
return this.client
|
||||
.api('me/memberOf')
|
||||
.version('v1.0')
|
||||
.get()
|
||||
.then((response): any => {
|
||||
let folders: Array<IFolder> = new Array<IFolder>();
|
||||
response.value.forEach((item) => {
|
||||
// Show unified Groups but NO Teams
|
||||
if (item['@odata.type'] === '#microsoft.graph.group') {
|
||||
if(!item.resourceProvisioningOptions || item.resourceProvisioningOptions.indexOf('Team') === -1) {
|
||||
folders.push({ id: item.id, name: item.displayName, driveID: item.id, parentFolder: null});
|
||||
}
|
||||
}
|
||||
});
|
||||
return folders;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function retrievs the user's membership groups from Graph
|
||||
*/
|
||||
public getJoinedTeams(): Promise<IFolder[]> {
|
||||
return this.client
|
||||
.api('me/joinedTeams')
|
||||
.version('v1.0')
|
||||
.get()
|
||||
.then((response): any => {
|
||||
let folders: Array<IFolder> = new Array<IFolder>();
|
||||
response.value.forEach((item) => {
|
||||
folders.push({ id: item.id, name: item.displayName, driveID: item.id, parentFolder: null});
|
||||
});
|
||||
return folders;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function retrieves all Drives for a given Group
|
||||
*/
|
||||
public getGroupDrives(group: IFolder): Promise<IFolder[]> {
|
||||
return this.client
|
||||
.api(`groups/${group.id}/drives`)
|
||||
.version('v1.0')
|
||||
.get()
|
||||
.then((response): any => {
|
||||
let folders: Array<IFolder> = new Array<IFolder>();
|
||||
response.value.forEach((item) => {
|
||||
folders.push({ id: item.id, name: item.name, driveID: item.id, parentFolder: group});
|
||||
});
|
||||
return folders;
|
||||
});
|
||||
}
|
||||
|
||||
public retrieveMimeMail(driveID: string, folderID: string, mail: IMail, clientCallback: (msg: string)=>void): Promise<string> {
|
||||
return this.client
|
||||
.api(`me/messages/${mail.id}/$value`)
|
||||
.version('v1.0')
|
||||
.responseType('TEXT')
|
||||
.get((err: any, response, rawResponse): any => {
|
||||
if (response.length < (4 * 1024 * 1024)) // If Mail size bigger 4MB use resumable upload
|
||||
{
|
||||
this.saveNormalMail(driveID, folderID, response, Utilities.createMailFileName(mail.subject), clientCallback);
|
||||
}
|
||||
else {
|
||||
this.saveBigMail(driveID, folderID, response, Utilities.createMailFileName(mail.subject), clientCallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private saveNormalMail(driveID: string, folderID: string, mimeStream: string, fileName: string, clientCallback: (msg: string)=>void) {
|
||||
const apiUrl = driveID !== folderID ? `drives/${driveID}/items/${folderID}:/${fileName}.eml:/content` : `drives/${driveID}/root:/${fileName}.eml:/content`;
|
||||
this.client
|
||||
.api(apiUrl)
|
||||
.put(mimeStream)
|
||||
.then((response) => {
|
||||
clientCallback('Success');
|
||||
})
|
||||
.catch((error) => {
|
||||
clientCallback('Error');
|
||||
});
|
||||
}
|
||||
|
||||
public async saveBigMail(driveID: string, folderID: string, mimeStream: string, fileName: string, clientCallback: (msg: string)=>void) {
|
||||
const sessionOptions = {
|
||||
"item": {
|
||||
"@microsoft.graph.conflictBehavior": "rename"
|
||||
}
|
||||
};
|
||||
const apiUrl = driveID !== folderID ? `drives/${driveID}/items/${folderID}:/${fileName}.eml:/createUploadSession` : `drives/${driveID}/root:/${fileName}.eml:/createUploadSession`;
|
||||
this.client
|
||||
.api(apiUrl)
|
||||
.post(JSON.stringify(sessionOptions))
|
||||
.then(async (response):Promise<any> => {
|
||||
console.log(response.uploadUrl);
|
||||
console.log(response.expirationDateTime);
|
||||
try {
|
||||
const resp = await this.uploadMailSlices(mimeStream, response.uploadUrl);
|
||||
console.log(resp);
|
||||
clientCallback('Success');
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
clientCallback('Error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async uploadMailSlices(mimeStream: string, uploadUrl: string) {
|
||||
let minSize=0;
|
||||
let maxSize=327680; // 320kb slices
|
||||
while(mimeStream.length > minSize) {
|
||||
const fileSlice = mimeStream.slice(minSize, maxSize);
|
||||
const resp = await this.uploadMailSlice(uploadUrl, minSize, maxSize, mimeStream.length, fileSlice);
|
||||
minSize = maxSize;
|
||||
maxSize += 327680;
|
||||
if (maxSize > mimeStream.length) {
|
||||
maxSize = mimeStream.length;
|
||||
}
|
||||
if (resp.id !== undefined) {
|
||||
return resp;
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadMailSlice(uploadUrl: string, minSize: number, maxSize: number, totalSize: number, fileSlice: string) {
|
||||
const header = {
|
||||
"Content-Length": `${maxSize - minSize}`,
|
||||
"Content-Range": `bytes ${minSize}-${maxSize-1}/${totalSize}`,
|
||||
};
|
||||
return await this.client
|
||||
.api(uploadUrl)
|
||||
.headers(header)
|
||||
.put(fileSlice);
|
||||
}
|
||||
|
||||
private saveMailCallback(error: any, response: any, rawResponse?: any): void {
|
||||
if (error !== null) {
|
||||
console.log(error);
|
||||
}
|
||||
else {
|
||||
console.log(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
|
||||
export default class Utilities {
|
||||
public static createMailFileName(subject: string): string {
|
||||
let fileName = subject.replace(/ /g, '_').replace(/:/g, '_');
|
||||
return fileName;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
export interface IFolder {
|
||||
name: string;
|
||||
id: string;
|
||||
driveID: string;
|
||||
parentFolder: IFolder;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface IMail {
|
||||
id: string;
|
||||
subject: string;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "7f1ef545-1d02-4cbd-b4e1-2f4140c1667a",
|
||||
"alias": "Outlook2SharePointWebPart",
|
||||
"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"],
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "Outlook 2 SharePoint" },
|
||||
"description": { "default": "Enables to copy mails fully to SharePoint, OneDrive, Teams" },
|
||||
"officeFabricIconFontName": "Page",
|
||||
"properties": {
|
||||
"description": "Outlook 2 SharePoint"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'Outlook2SharePointWebPartStrings';
|
||||
import { IMail } from '../../model/IMail';
|
||||
import Outlook2SharePoint from './components/Outlook2SharePoint';
|
||||
import { IOutlook2SharePointProps } from './components/IOutlook2SharePointProps';
|
||||
|
||||
export interface IOutlook2SharePointWebPartProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default class Outlook2SharePointWebPart extends BaseClientSideWebPart <IOutlook2SharePointWebPartProps> {
|
||||
public render(): void {
|
||||
let mail: IMail = null;
|
||||
if (this.context.sdks.office) {
|
||||
const item = this.context.sdks.office.context.mailbox.item;
|
||||
if (item !== null) {
|
||||
mail = { id: item.itemId,subject: item.subject };
|
||||
}
|
||||
}
|
||||
|
||||
const element: React.ReactElement<IOutlook2SharePointProps> = React.createElement(
|
||||
Outlook2SharePoint,
|
||||
{
|
||||
msGraphClientFactory: this.context.msGraphClientFactory,
|
||||
mail: mail
|
||||
}
|
||||
);
|
||||
|
||||
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('description', {
|
||||
label: strings.DescriptionFieldLabel
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.folder {
|
||||
.header {
|
||||
@include ms-fontSize-m;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.isLink {
|
||||
cursor: pointer;
|
||||
}
|
||||
.sublist {
|
||||
list-style-type: none;
|
||||
margin-inline-start: 20px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from 'react';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import styles from './Folder.module.scss';
|
||||
import { IFolderProps } from './IFolderProps';
|
||||
|
||||
export default class Folder extends React.Component<IFolderProps, {}> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IFolderProps> {
|
||||
return (
|
||||
<li className={styles.folder}>
|
||||
<Icon iconName="DocLibrary" className="ms-IconDocLibrary" />
|
||||
<span className={`${styles.header} ${styles.isLink}`} onClick={this.getSubFolder}>{this.props.folder.name}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
private getSubFolder = () => {
|
||||
this.props.subFolderCallback(this.props.folder);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.groups {
|
||||
margin-bottom: 10px;
|
||||
.header {
|
||||
@include ms-fontSize-l;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.list {
|
||||
list-style-type: none;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
.saveBtn {
|
||||
margin: 5px 10px 5px 10px;
|
||||
}
|
||||
.spinnerContainer {
|
||||
position: relative;
|
||||
@include ms-md12;
|
||||
@include ms-lg10;
|
||||
@include ms-xl8;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
import * as React from 'react';
|
||||
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import Breadcrumb from './controls/Breadcrumb';
|
||||
import Folder from './Folder';
|
||||
import styles from './Groups.module.scss';
|
||||
import { IGroupsProps } from './IGroupsProps';
|
||||
import { IGroupsState } from './IGroupsState';
|
||||
import { IFolder } from '../../../model/IFolder';
|
||||
|
||||
export default class Groups extends React.Component<IGroupsProps, IGroupsState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
folders: [],
|
||||
grandParentFolder: null,
|
||||
parentFolder: null,
|
||||
showSpinner: false
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.graphController !== null) {
|
||||
this.getGroups();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IGroupsProps> {
|
||||
let fldrs = this.state.folders.map((fldr) => {
|
||||
return <Folder folder={fldr} subFolderCallback={fldr.parentFolder===null?this.getGroupDrives:this.getSubFolders}></Folder>;
|
||||
});
|
||||
return (
|
||||
<div className={styles.groups}>
|
||||
<div>
|
||||
<div>
|
||||
<Breadcrumb
|
||||
grandParentFolder={this.state.grandParentFolder}
|
||||
parentFolder={this.state.parentFolder}
|
||||
rootCallback={this.showRoot}
|
||||
parentFolderCallback={this.showParentFolder}>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-content">
|
||||
<ul className={styles.list}>
|
||||
{fldrs}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton
|
||||
className={styles.saveBtn}
|
||||
text="Save here"
|
||||
onClick={this.saveMailTo}
|
||||
disabled={this.state.parentFolder === null}
|
||||
allowDisabledFocus={true}
|
||||
/>
|
||||
{ this.state.showSpinner && (
|
||||
<div className={styles.spinnerContainer}>
|
||||
<Overlay >
|
||||
<Spinner size={ SpinnerSize.large } label='Processing request' />
|
||||
</Overlay>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getGroups = () => {
|
||||
this.props.graphController.getJoinedGroups().then((folders) => {
|
||||
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
|
||||
return {
|
||||
folders: folders
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getGroupDrives = (group: IFolder) => {
|
||||
let nextParent: IFolder = null;
|
||||
this.state.folders.forEach((fldr) => {
|
||||
if (fldr.id === group.id) {
|
||||
nextParent = fldr;
|
||||
}
|
||||
});
|
||||
this.props.graphController.getGroupDrives(group).then((folders) => {
|
||||
if (folders.length > 0) {
|
||||
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
|
||||
return {
|
||||
folders: folders,
|
||||
grandParentFolder: null,
|
||||
parentFolder: group
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getSubFolders = (folder: IFolder) => {
|
||||
if (folder.id === folder.driveID) {
|
||||
this.props.graphController.getGroupRootFolders(folder).then((folders) => {
|
||||
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
|
||||
return {
|
||||
folders: folders,
|
||||
grandParentFolder: folder.parentFolder,
|
||||
parentFolder: folder
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.props.graphController.getSubFolder(folder).then((folders) => {
|
||||
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
|
||||
return {
|
||||
folders: folders,
|
||||
grandParentFolder: folder.parentFolder,
|
||||
parentFolder: folder
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private showRoot = () => {
|
||||
this.getGroups();
|
||||
}
|
||||
|
||||
private showParentFolder = (parentFolder: IFolder) => {
|
||||
if (this.state.grandParentFolder===null) {
|
||||
this.getGroupDrives(parentFolder);
|
||||
}
|
||||
else {
|
||||
this.getSubFolders(parentFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private saveMailTo = () => {
|
||||
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
|
||||
return {
|
||||
showSpinner: true
|
||||
};
|
||||
});
|
||||
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback);
|
||||
}
|
||||
|
||||
private saveMailCallback = (message: string) => {
|
||||
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
|
||||
return {
|
||||
showSpinner: false
|
||||
};
|
||||
});
|
||||
if (message.indexOf('Success') > -1) {
|
||||
this.props.successCallback(message);
|
||||
}
|
||||
else {
|
||||
this.props.errorCallback(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IFolder } from '../../../model/IFolder';
|
||||
|
||||
export interface IFolderProps {
|
||||
folder: IFolder;
|
||||
subFolderCallback: (folder: IFolder) => void;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import GraphController from '../../../controller/GraphController';
|
||||
import { IMail } from '../../../model/IMail';
|
||||
|
||||
|
||||
export interface IGroupsProps {
|
||||
graphController: GraphController;
|
||||
mail: IMail;
|
||||
successCallback: (msg: string) => void;
|
||||
errorCallback: (msg: string) => void;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { IFolder } from '../../../model/IFolder';
|
||||
|
||||
export interface IGroupsState {
|
||||
folders: IFolder[];
|
||||
grandParentFolder: IFolder;
|
||||
parentFolder: IFolder;
|
||||
showSpinner: boolean;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import GraphController from '../../../controller/GraphController';
|
||||
import { IMail } from '../../../model/IMail';
|
||||
|
||||
export interface IOneDriveProps {
|
||||
graphController: GraphController;
|
||||
mail: IMail;
|
||||
successCallback: (msg: string) => void;
|
||||
errorCallback: (msg: string) => void;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { IFolder } from '../../../model/IFolder';
|
||||
|
||||
export interface IOneDriveState {
|
||||
folders: IFolder[];
|
||||
grandParentFolder: IFolder;
|
||||
parentFolder: IFolder;
|
||||
showSpinner: boolean;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { MSGraphClientFactory } from '@microsoft/sp-http';
|
||||
import { IMail } from '../../../model/IMail';
|
||||
|
||||
export interface IOutlook2SharePointProps {
|
||||
mail: IMail;
|
||||
msGraphClientFactory: MSGraphClientFactory;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import GraphController from '../../../controller/GraphController';
|
||||
|
||||
export interface IOutlook2SharePointState {
|
||||
graphController: GraphController;
|
||||
showSuccess: boolean;
|
||||
showError: boolean;
|
||||
showOneDrive: boolean;
|
||||
showTeams: boolean;
|
||||
showGroups: boolean;
|
||||
successMessage: string;
|
||||
errorMessage: string;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import GraphController from '../../../controller/GraphController';
|
||||
import { IMail } from '../../../model/IMail';
|
||||
|
||||
|
||||
export interface ITeamsProps {
|
||||
graphController: GraphController;
|
||||
mail: IMail;
|
||||
successCallback: (msg: string) => void;
|
||||
errorCallback: (msg: string) => void;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { IFolder } from '../../../model/IFolder';
|
||||
|
||||
export interface ITeamsState {
|
||||
folders: IFolder[];
|
||||
grandParentFolder: IFolder;
|
||||
parentFolder: IFolder;
|
||||
showSpinner: boolean;
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import * as React from 'react';
|
||||
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import Folder from './Folder';
|
||||
import styles from './Groups.module.scss';
|
||||
import Breadcrumb from './controls/Breadcrumb';
|
||||
import { IOneDriveProps } from './IOneDriveProps';
|
||||
import { IOneDriveState } from './IOneDriveState';
|
||||
import { IFolder } from '../../../model/IFolder';
|
||||
|
||||
export default class OneDrive extends React.Component<IOneDriveProps, IOneDriveState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
folders: [],
|
||||
grandParentFolder: null,
|
||||
parentFolder: null,
|
||||
showSpinner: false
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.graphController !== null) {
|
||||
this.getFolder();
|
||||
}
|
||||
}
|
||||
public render(): React.ReactElement<IOneDriveProps> {
|
||||
let fldrs = this.state.folders.map((fldr) => {
|
||||
return <Folder folder={fldr} subFolderCallback={this.getSubFolders}></Folder>;
|
||||
});
|
||||
return (
|
||||
<div className={styles.groups}>
|
||||
<div>
|
||||
<div>
|
||||
<Breadcrumb
|
||||
grandParentFolder={this.state.grandParentFolder}
|
||||
parentFolder={this.state.parentFolder}
|
||||
rootCallback={this.showRoot}
|
||||
parentFolderCallback={this.showParentFolder}>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-content">
|
||||
<ul className={styles.list}>
|
||||
{fldrs}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton
|
||||
className={styles.saveBtn}
|
||||
text="Save here"
|
||||
onClick={this.saveMailTo}
|
||||
allowDisabledFocus={true}
|
||||
/>
|
||||
{ this.state.showSpinner && (
|
||||
<div className={styles.spinnerContainer}>
|
||||
<Overlay >
|
||||
<Spinner size={ SpinnerSize.large } label='Processing request' />
|
||||
</Overlay>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getFolder = () => {
|
||||
this.props.graphController.getOneDriveFolder().then((folders) => {
|
||||
this.setState((prevState: IOneDriveState, props: IOneDriveProps) => {
|
||||
return {
|
||||
folders: folders
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getSubFolders = (folder: IFolder) => {
|
||||
this.props.graphController.getSubFolder(folder).then((folders) => {
|
||||
if (folders.length > 0) {
|
||||
this.setState((prevState: IOneDriveState, props: IOneDriveProps) => {
|
||||
return {
|
||||
folders: folders,
|
||||
grandParentFolder: folder.parentFolder,
|
||||
parentFolder: folder
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showRoot = () => {
|
||||
this.getFolder();
|
||||
}
|
||||
|
||||
private showParentFolder = (parentFolder: IFolder) => {
|
||||
this.getSubFolders(parentFolder);
|
||||
}
|
||||
|
||||
private saveMailTo = () => {
|
||||
this.setState((prevState: IOneDriveState, props: IOneDriveProps) => {
|
||||
return {
|
||||
showSpinner: true
|
||||
};
|
||||
});
|
||||
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback);
|
||||
}
|
||||
|
||||
private saveMailCallback = (message: string) => {
|
||||
this.setState((prevState: IOneDriveState, props: IOneDriveProps) => {
|
||||
return {
|
||||
showSpinner: false
|
||||
};
|
||||
});
|
||||
if (message.indexOf('Success') > -1) {
|
||||
this.props.successCallback(message);
|
||||
}
|
||||
else {
|
||||
this.props.errorCallback(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.outlook2SharePoint {
|
||||
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
||||
.header {
|
||||
cursor: pointer;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.headerIcon {
|
||||
font-size: 1.5em;
|
||||
font-weight: bolder;
|
||||
}
|
||||
.headerText {
|
||||
@include ms-fontSize-l;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
import * as React from 'react';
|
||||
import styles from './Outlook2SharePoint.module.scss';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
||||
import GraphController from '../../../controller/GraphController';
|
||||
import Groups from './Groups';
|
||||
import OneDrive from './OneDrive';
|
||||
import Teams from './Teams';
|
||||
import { IOutlook2SharePointProps } from './IOutlook2SharePointProps';
|
||||
import { IOutlook2SharePointState } from './IOutlook2SharePointState';
|
||||
|
||||
export default class Outlook2SharePoint extends React.Component<IOutlook2SharePointProps, IOutlook2SharePointState> {
|
||||
private graphController: GraphController;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
graphController: null,
|
||||
showError: false,
|
||||
showSuccess: false,
|
||||
showOneDrive: false,
|
||||
showTeams: false,
|
||||
showGroups: false,
|
||||
successMessage: '',
|
||||
errorMessage: ''
|
||||
};
|
||||
this.graphController = new GraphController(this.props.msGraphClientFactory, this.graphClientReadyCallback);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IOutlook2SharePointProps> {
|
||||
|
||||
return (
|
||||
<div className={ styles.outlook2SharePoint }>
|
||||
{this.state.showSuccess && <div>
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.success}
|
||||
isMultiline={false}
|
||||
onDismiss={this.closeMessage}
|
||||
dismissButtonAriaLabel="Close"
|
||||
truncated={true}
|
||||
overflowButtonAriaLabel="See more"
|
||||
>
|
||||
{this.state.successMessage}
|
||||
</MessageBar>
|
||||
</div>}
|
||||
{this.state.showError && <div>
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
onDismiss={this.closeMessage}
|
||||
dismissButtonAriaLabel="Close"
|
||||
truncated={true}
|
||||
overflowButtonAriaLabel="See more"
|
||||
>
|
||||
{this.state.errorMessage}
|
||||
</MessageBar>
|
||||
</div>}
|
||||
<div className={styles.header} onClick={this.showOneDrive}>
|
||||
<Icon iconName="OneDrive" className={`ms-IconOneDrive ${styles.headerIcon}`} />
|
||||
<span className={styles.headerText}>OneDrive</span>
|
||||
</div>
|
||||
{this.state.graphController && this.state.showOneDrive &&
|
||||
<OneDrive
|
||||
graphController={this.state.graphController}
|
||||
mail={this.props.mail}
|
||||
successCallback={this.showSuccess}
|
||||
errorCallback={this.showError}>
|
||||
</OneDrive>}
|
||||
|
||||
<div className={styles.header} onClick={this.showTeams}>
|
||||
<Icon iconName="TeamsLogo" className={`ms-IconTeamsLogo ${styles.headerIcon}`} />
|
||||
<span className={styles.headerText}>Microsoft Teams</span>
|
||||
</div>
|
||||
{this.state.graphController && this.state.showTeams &&
|
||||
<Teams
|
||||
graphController={this.state.graphController}
|
||||
mail={this.props.mail}
|
||||
successCallback={this.showSuccess}
|
||||
errorCallback={this.showError}>
|
||||
</Teams>}
|
||||
<div className={styles.header} onClick={this.showGroups}>
|
||||
<Icon iconName="Group" className={`ms-IconGroup ${styles.headerIcon}`} />
|
||||
<span className={styles.headerText}>Microsoft Groups</span>
|
||||
</div>
|
||||
{this.state.graphController && this.state.showGroups &&
|
||||
<Groups
|
||||
graphController={this.state.graphController}
|
||||
mail={this.props.mail}
|
||||
successCallback={this.showSuccess}
|
||||
errorCallback={this.showError}>
|
||||
</Groups>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function first retrieves all OneDrive root folders from user
|
||||
*/
|
||||
private graphClientReadyCallback = () => {
|
||||
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
|
||||
return {
|
||||
graphController: this.graphController
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private showError = (message: string) => {
|
||||
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
|
||||
return {
|
||||
showError: true,
|
||||
showSuccess: false,
|
||||
errorMessage: message
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private showSuccess = (message: string) => {
|
||||
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
|
||||
return {
|
||||
showSuccess: true,
|
||||
showError: false,
|
||||
successMessage: message
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private closeMessage = () => {
|
||||
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
|
||||
return {
|
||||
showSuccess: false,
|
||||
showError: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function expands the Teams section and collapses the other ones
|
||||
*/
|
||||
private showTeams = () => {
|
||||
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
|
||||
return {
|
||||
showTeams: true,
|
||||
showOneDrive: false,
|
||||
showGroups: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function expands the OneDrive section and collapses the other ones
|
||||
*/
|
||||
private showOneDrive = () => {
|
||||
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
|
||||
return {
|
||||
showOneDrive: true,
|
||||
showTeams: false,
|
||||
showGroups: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function expands the Groups section and collapses the other ones
|
||||
*/
|
||||
private showGroups = () => {
|
||||
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
|
||||
return {
|
||||
showGroups: true,
|
||||
showTeams: false,
|
||||
showOneDrive: false
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
import * as React from 'react';
|
||||
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import Breadcrumb from './controls/Breadcrumb';
|
||||
import Folder from './Folder';
|
||||
import styles from './Groups.module.scss';
|
||||
import { ITeamsProps } from './ITeamsProps';
|
||||
import { ITeamsState } from './ITeamsState';
|
||||
import { IFolder } from '../../../model/IFolder';
|
||||
|
||||
export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
folders: [],
|
||||
grandParentFolder: null,
|
||||
parentFolder: null,
|
||||
showSpinner: false
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.graphController !== null) {
|
||||
this.getTeams();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<ITeamsState> {
|
||||
let fldrs = this.state.folders.map((fldr) => {
|
||||
return <Folder folder={fldr} subFolderCallback={fldr.parentFolder===null?this.getGroupDrives:this.getSubFolders}></Folder>;
|
||||
});
|
||||
return (
|
||||
<div className={styles.groups}>
|
||||
<div>
|
||||
<div>
|
||||
<Breadcrumb
|
||||
grandParentFolder={this.state.grandParentFolder}
|
||||
parentFolder={this.state.parentFolder}
|
||||
rootCallback={this.showRoot}
|
||||
parentFolderCallback={this.showParentFolder}>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-content">
|
||||
<ul className={styles.list}>
|
||||
{fldrs}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton
|
||||
className={styles.saveBtn}
|
||||
text="Save here"
|
||||
onClick={this.saveMailTo}
|
||||
disabled={this.state.parentFolder === null}
|
||||
allowDisabledFocus={true}
|
||||
/>
|
||||
{ this.state.showSpinner && (
|
||||
<div className={styles.spinnerContainer}>
|
||||
<Overlay >
|
||||
<Spinner size={ SpinnerSize.large } label='Processing request' />
|
||||
</Overlay>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getTeams = () => {
|
||||
this.props.graphController.getJoinedTeams().then((folders) => {
|
||||
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
|
||||
return {
|
||||
folders: folders
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getGroupDrives = (group: IFolder) => {
|
||||
let nextParent: IFolder = null;
|
||||
this.state.folders.forEach((fldr) => {
|
||||
if (fldr.id === group.id) {
|
||||
nextParent = fldr;
|
||||
}
|
||||
});
|
||||
this.props.graphController.getGroupDrives(group).then((folders) => {
|
||||
if (folders.length > 0) {
|
||||
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
|
||||
return {
|
||||
folders: folders,
|
||||
grandParentFolder: null,
|
||||
parentFolder: group
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getSubFolders = (folder: IFolder) => {
|
||||
if (folder.id === folder.driveID) {
|
||||
this.props.graphController.getGroupRootFolders(folder).then((folders) => {
|
||||
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
|
||||
return {
|
||||
folders: folders,
|
||||
grandParentFolder: folder.parentFolder,
|
||||
parentFolder: folder
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.props.graphController.getSubFolder(folder).then((folders) => {
|
||||
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
|
||||
return {
|
||||
folders: folders,
|
||||
grandParentFolder: folder.parentFolder,
|
||||
parentFolder: folder
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private showRoot = () => {
|
||||
this.getTeams();
|
||||
}
|
||||
|
||||
private showParentFolder = (parentFolder: IFolder) => {
|
||||
if (this.state.grandParentFolder===null) {
|
||||
this.getGroupDrives(parentFolder);
|
||||
}
|
||||
else {
|
||||
this.getSubFolders(parentFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private saveMailTo = () => {
|
||||
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
|
||||
return {
|
||||
showSpinner: true
|
||||
};
|
||||
});
|
||||
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback);
|
||||
}
|
||||
|
||||
private saveMailCallback = (message: string) => {
|
||||
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
|
||||
return {
|
||||
showSpinner: false
|
||||
};
|
||||
});
|
||||
if (message.indexOf('Success') > -1) {
|
||||
this.props.successCallback(message);
|
||||
}
|
||||
else {
|
||||
this.props.errorCallback(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.breadcrumb {
|
||||
@include ms-Grid;
|
||||
.rootIcon {
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
@include ms-Grid-col;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.row {
|
||||
@include ms-Grid-row;
|
||||
}
|
||||
.grandParent {
|
||||
@include ms-Grid-col;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
.nonLink {
|
||||
margin-left: 5px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import * as React from 'react';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import styles from './Breadcrumb.module.scss';
|
||||
import { IBreadcrumbProps } from './IBreadcrumbProps';
|
||||
//import { IFolderState } from './IFolderState';
|
||||
|
||||
export default class Breadcrumb extends React.Component<IBreadcrumbProps, {}> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
subFolders: []
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IBreadcrumbProps> {
|
||||
return (
|
||||
<div className={styles.breadcrumb}>
|
||||
{this.props.grandParentFolder !== null && this.props.parentFolder !== null &&
|
||||
<Icon onClick={this.showRoot} iconName="DoubleChevronLeft" className={`ms-IconDoubleChevronLeft ${styles.rootIcon}`} />}
|
||||
<div className={styles.row}>
|
||||
{this.props.grandParentFolder &&
|
||||
<div className={styles.grandParent}>
|
||||
<span className={styles.link} onClick={this.showParentFolder}>{this.props.grandParentFolder.name}</span>
|
||||
</div>}
|
||||
{this.props.parentFolder &&
|
||||
<div className={styles.grandParent}>
|
||||
<Icon iconName="ChevronRight" className="ms-IconChevronRight" />
|
||||
<span className={styles.nonLink}>{this.props.parentFolder.name}</span>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private showRoot = () => {
|
||||
this.props.rootCallback();
|
||||
}
|
||||
|
||||
private showParentFolder = () => {
|
||||
this.props.parentFolderCallback(this.props.grandParentFolder);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { IFolder } from '../../../../model/IFolder';
|
||||
|
||||
export interface IBreadcrumbProps {
|
||||
grandParentFolder: IFolder;
|
||||
parentFolder: IFolder;
|
||||
rootCallback: () => void;
|
||||
parentFolderCallback: (folder: IFolder) => void;
|
||||
}
|
7
samples/react-outlook-copy2teams/src/webparts/outlook2SharePoint/loc/en-us.js
vendored
Normal file
7
samples/react-outlook-copy2teams/src/webparts/outlook2SharePoint/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
10
samples/react-outlook-copy2teams/src/webparts/outlook2SharePoint/loc/mystrings.d.ts
vendored
Normal file
10
samples/react-outlook-copy2teams/src/webparts/outlook2SharePoint/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare interface IOutlook2SharePointWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'Outlook2SharePointWebPartStrings' {
|
||||
const strings: IOutlook2SharePointWebPartStrings;
|
||||
export = strings;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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": [
|
||||
"es6-promise",
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "@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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue