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:
Markus Möller 2020-02-08 11:50:01 +01:00 committed by GitHub
parent dc6bfe633b
commit cead35f5db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 18618 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

View File

@ -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" />

View File

@ -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"
}
}

View File

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

View File

@ -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 -->"
}

View File

@ -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"
}
}

View File

@ -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/"
}
}

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,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'));

View File

@ -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&amp;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&amp;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

View File

@ -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"
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,8 @@
export default class Utilities {
public static createMailFileName(subject: string): string {
let fileName = subject.replace(/ /g, '_').replace(/:/g, '_');
return fileName;
}
}

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,7 @@
export interface IFolder {
name: string;
id: string;
driveID: string;
parentFolder: IFolder;
}

View File

@ -0,0 +1,4 @@
export interface IMail {
id: string;
subject: string;
}

View File

@ -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"
}
}]
}

View File

@ -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
})
]
}
]
}
]
};
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,6 @@
import { IFolder } from '../../../model/IFolder';
export interface IFolderProps {
folder: IFolder;
subFolderCallback: (folder: IFolder) => void;
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
import { IFolder } from '../../../model/IFolder';
export interface IGroupsState {
folders: IFolder[];
grandParentFolder: IFolder;
parentFolder: IFolder;
showSpinner: boolean;
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
import { IFolder } from '../../../model/IFolder';
export interface IOneDriveState {
folders: IFolder[];
grandParentFolder: IFolder;
parentFolder: IFolder;
showSpinner: boolean;
}

View File

@ -0,0 +1,7 @@
import { MSGraphClientFactory } from '@microsoft/sp-http';
import { IMail } from '../../../model/IMail';
export interface IOutlook2SharePointProps {
mail: IMail;
msGraphClientFactory: MSGraphClientFactory;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
import { IFolder } from '../../../model/IFolder';
export interface ITeamsState {
folders: IFolder[];
grandParentFolder: IFolder;
parentFolder: IFolder;
showSpinner: boolean;
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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
};
});
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,8 @@
import { IFolder } from '../../../../model/IFolder';
export interface IBreadcrumbProps {
grandParentFolder: IFolder;
parentFolder: IFolder;
rootCallback: () => void;
parentFolderCallback: (folder: IFolder) => void;
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View 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

View File

@ -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"
]
}

View File

@ -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
}
}