Open extensions added to store metadata to saved mail

This commit is contained in:
Markus Moeller 2020-04-06 18:14:16 +02:00
parent e4e1a044b1
commit 6fd6866903
16 changed files with 229 additions and 96 deletions

View File

@ -1,4 +1,4 @@
## outlook-2-teams-spfx ## outlook-2-sp-spfx
## Summary ## Summary
@ -8,7 +8,7 @@ Furthermore it shows you how to retrieve a complete mail as a mimestream via Mic
* Writing normal files smaller 4MB * Writing normal files smaller 4MB
* Writing big files with an UploadSession when bigger than 4MB * Writing big files with an UploadSession when bigger than 4MB
## outlook-2-teams-spfx in action ## outlook-2-sp-spfx in action
![WebPartInAction](https://mmsharepoint.files.wordpress.com/2020/01/addin_overall.png) ![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/) 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/)
@ -25,13 +25,14 @@ A detailed functionality and technical description can be found in the [author's
Solution|Author(s) Solution|Author(s)
--------|--------- --------|---------
outlook-2-teams-spfx| Markus Moeller ([@moeller2_0](http://www.twitter.com/moeller2_0)) outlook-2-sp-spfx| Markus Moeller ([@moeller2_0](http://www.twitter.com/moeller2_0))
## Version history ## Version history
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0|February 05, 2020|Initial release 1.0|January 29, 2020|Initial release
1.1|April 06, 2020|Open extensions to store metadata added
## Disclaimer ## Disclaimer
@ -68,5 +69,4 @@ This Outlook Add-In shows the following capabilities on top of the SharePoint Fr
* Use Microsoft Graph to retrieve folders and subfolders for OneDrive or Teams/Group drives * 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 retrieve complete mail mimestream by given ID
* Use Microsoft Graph to save normal or big files (in size bigger 4MB) with different concepts * Use Microsoft Graph to save normal or big files (in size bigger 4MB) with different concepts
* Optionally store metadata of save operation to copied mail with open extension (configure line 15 Outlook2SharePoint.tsx)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-outlook-copy2teams" />

View File

@ -26,7 +26,7 @@
}, },
{ {
"resource": "Microsoft Graph", "resource": "Microsoft Graph",
"scope": "Mail.Read" "scope": "Mail.ReadWrite"
}, },
{ {
"resource": "Microsoft Graph", "resource": "Microsoft Graph",

View File

@ -2,19 +2,27 @@ import { MSGraphClient, MSGraphClientFactory } from '@microsoft/sp-http';
import Utilities from './Utilities'; import Utilities from './Utilities';
import { IFolder } from '../model/IFolder'; import { IFolder } from '../model/IFolder';
import { IMail } from '../model/IMail'; import { IMail } from '../model/IMail';
import { IMailMetadata } from '../model/IMailMetadata';
export default class GraphController { export default class GraphController {
private client: MSGraphClient; private client: MSGraphClient;
private metadataExtensionName = 'mmsharepoint.onmicrosoft.MailStorage';
private saveMetadata: boolean;
constructor (graphFactory: MSGraphClientFactory, callback: () => void) { constructor (saveMetadata: boolean) {
graphFactory this.saveMetadata = saveMetadata;
}
public init(graphFactory: MSGraphClientFactory): Promise<boolean> {
return graphFactory
.getClient() .getClient()
.then((client: MSGraphClient) => { .then((client: MSGraphClient) => {
this.client = client; this.client = client;
callback(); return true;
})
.catch((error) => {
return false;
}); });
this.retrieveMimeMail = this.retrieveMimeMail.bind(this);
} }
public getClient() { public getClient() {
@ -29,11 +37,12 @@ export default class GraphController {
.api('me/drive/root/children') .api('me/drive/root/children')
.version('v1.0') .version('v1.0')
.filter('folder ne null') .filter('folder ne null')
.select('id, name, parentReference, webUrl')
.get() .get()
.then((response): any => { .then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>(); let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => { response.value.forEach((item) => {
folders.push({ id: item.id, name: item.name, driveID: item.parentReference.driveId, parentFolder: null}); folders.push({ id: item.id, name: item.name, driveID: item.parentReference.driveId, parentFolder: null, webUrl: item.webUrl });
}); });
return folders; return folders;
}); });
@ -44,11 +53,12 @@ export default class GraphController {
.api(`drives/${group.driveID}/root/children`) .api(`drives/${group.driveID}/root/children`)
.version('v1.0') .version('v1.0')
.filter('folder ne null') .filter('folder ne null')
.select('id, name, webUrl')
.get() .get()
.then((response): any => { .then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>(); let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => { response.value.forEach((item) => {
folders.push({ id: item.id, name: item.name, driveID: group.driveID, parentFolder: group}); folders.push({ id: item.id, name: item.name, driveID: group.driveID, parentFolder: group, webUrl: item.webUrl});
}); });
return folders; return folders;
}); });
@ -59,11 +69,12 @@ export default class GraphController {
.api(`drives/${folder.driveID}/items/${folder.id}/children`) .api(`drives/${folder.driveID}/items/${folder.id}/children`)
.version('v1.0') .version('v1.0')
.filter('folder ne null') .filter('folder ne null')
.select('id, name, webUrl')
.get() .get()
.then((response): any => { .then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>(); let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => { response.value.forEach((item) => {
folders.push({ id: item.id, name: item.name, driveID: folder.driveID, parentFolder: folder}); folders.push({ id: item.id, name: item.name, driveID: folder.driveID, parentFolder: folder, webUrl: item.webUrl});
}); });
return folders; return folders;
}); });
@ -76,6 +87,7 @@ export default class GraphController {
return this.client return this.client
.api('me/memberOf') .api('me/memberOf')
.version('v1.0') .version('v1.0')
.select('id, displayName, webUrl')
.get() .get()
.then((response): any => { .then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>(); let folders: Array<IFolder> = new Array<IFolder>();
@ -83,7 +95,7 @@ export default class GraphController {
// Show unified Groups but NO Teams // Show unified Groups but NO Teams
if (item['@odata.type'] === '#microsoft.graph.group') { if (item['@odata.type'] === '#microsoft.graph.group') {
if(!item.resourceProvisioningOptions || item.resourceProvisioningOptions.indexOf('Team') === -1) { if(!item.resourceProvisioningOptions || item.resourceProvisioningOptions.indexOf('Team') === -1) {
folders.push({ id: item.id, name: item.displayName, driveID: item.id, parentFolder: null}); folders.push({ id: item.id, name: item.displayName, driveID: item.id, parentFolder: null, webUrl: item.webUrl});
} }
} }
}); });
@ -98,11 +110,12 @@ export default class GraphController {
return this.client return this.client
.api('me/joinedTeams') .api('me/joinedTeams')
.version('v1.0') .version('v1.0')
.select('id, displayName, webUrl')
.get() .get()
.then((response): any => { .then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>(); let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => { response.value.forEach((item) => {
folders.push({ id: item.id, name: item.displayName, driveID: item.id, parentFolder: null}); folders.push({ id: item.id, name: item.displayName, driveID: item.id, parentFolder: null, webUrl: item.webUrl});
}); });
return folders; return folders;
}); });
@ -115,42 +128,46 @@ export default class GraphController {
return this.client return this.client
.api(`groups/${group.id}/drives`) .api(`groups/${group.id}/drives`)
.version('v1.0') .version('v1.0')
.select('id, name, webUrl')
.get() .get()
.then((response): any => { .then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>(); let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => { response.value.forEach((item) => {
folders.push({ id: item.id, name: item.name, driveID: item.id, parentFolder: group}); folders.push({ id: item.id, name: item.name, driveID: item.id, parentFolder: group, webUrl: item.webUrl});
}); });
return folders; return folders;
}); });
} }
public retrieveMimeMail(driveID: string, folderID: string, mail: IMail, clientCallback: (msg: string)=>void): Promise<string> { public retrieveMimeMail = (driveID: string, folderID: string, mail: IMail, clientCallback: (msg: string)=>void): Promise<string> => {
return this.client return this.client
.api(`me/messages/${mail.id}/$value`) .api(`me/messages/${mail.id}/$value`)
.version('v1.0') .version('v1.0')
.responseType('TEXT') .responseType('TEXT')
.get((err: any, response, rawResponse): any => { .get()
.then((response): any => {
if (response.length < (4 * 1024 * 1024)) // If Mail size bigger 4MB use resumable upload if (response.length < (4 * 1024 * 1024)) // If Mail size bigger 4MB use resumable upload
{ {
this.saveNormalMail(driveID, folderID, response, Utilities.createMailFileName(mail.subject), clientCallback); return this.saveNormalMail(driveID, folderID, response, Utilities.createMailFileName(mail.subject), clientCallback);
} }
else { else {
this.saveBigMail(driveID, folderID, response, Utilities.createMailFileName(mail.subject), clientCallback); return this.saveBigMail(driveID, folderID, response, Utilities.createMailFileName(mail.subject), clientCallback);
} }
}); });
} }
private saveNormalMail(driveID: string, folderID: string, mimeStream: string, fileName: string, clientCallback: (msg: string)=>void) { private saveNormalMail(driveID: string, folderID: string, mimeStream: string, fileName: string, clientCallback: (msg: string)=>void): Promise<string> {
const apiUrl = driveID !== folderID ? `drives/${driveID}/items/${folderID}:/${fileName}.eml:/content` : `drives/${driveID}/root:/${fileName}.eml:/content`; const apiUrl = driveID !== folderID ? `drives/${driveID}/items/${folderID}:/${fileName}.eml:/content` : `drives/${driveID}/root:/${fileName}.eml:/content`;
this.client return this.client
.api(apiUrl) .api(apiUrl)
.put(mimeStream) .put(mimeStream)
.then((response) => { .then((response) => {
clientCallback('Success'); clientCallback('Success');
return 'Success';
}) })
.catch((error) => { .catch((error) => {
clientCallback('Error'); clientCallback('Error');
return null;
}); });
} }
@ -161,32 +178,30 @@ export default class GraphController {
} }
}; };
const apiUrl = driveID !== folderID ? `drives/${driveID}/items/${folderID}:/${fileName}.eml:/createUploadSession` : `drives/${driveID}/root:/${fileName}.eml:/createUploadSession`; const apiUrl = driveID !== folderID ? `drives/${driveID}/items/${folderID}:/${fileName}.eml:/createUploadSession` : `drives/${driveID}/root:/${fileName}.eml:/createUploadSession`;
this.client return this.client
.api(apiUrl) .api(apiUrl)
.post(JSON.stringify(sessionOptions)) .post(JSON.stringify(sessionOptions))
.then(async (response):Promise<any> => { .then(async (response):Promise<any> => {
console.log(response.uploadUrl);
console.log(response.expirationDateTime);
try { try {
const resp = await this.uploadMailSlices(mimeStream, response.uploadUrl); const resp = await this.uploadMailSlices(mimeStream, response.uploadUrl);
console.log(resp);
clientCallback('Success'); clientCallback('Success');
return 'Success';
} }
catch(err) { catch(err) {
console.log(err);
clientCallback('Error'); clientCallback('Error');
return null;
} }
}); });
} }
private async uploadMailSlices(mimeStream: string, uploadUrl: string) { private async uploadMailSlices(mimeStream: string, uploadUrl: string) {
let minSize=0; let minSize=0;
let maxSize=327680; // 320kb slices let maxSize=5*327680; // 5*320kb slices --> MUST be a multiple of 320 KiB (327,680 bytes)
while(mimeStream.length > minSize) { while(mimeStream.length > minSize) {
const fileSlice = mimeStream.slice(minSize, maxSize); const fileSlice = mimeStream.slice(minSize, maxSize);
const resp = await this.uploadMailSlice(uploadUrl, minSize, maxSize, mimeStream.length, fileSlice); const resp = await this.uploadMailSlice(uploadUrl, minSize, maxSize, mimeStream.length, fileSlice);
minSize = maxSize; minSize = maxSize;
maxSize += 327680; maxSize += 5*327680;
if (maxSize > mimeStream.length) { if (maxSize > mimeStream.length) {
maxSize = mimeStream.length; maxSize = mimeStream.length;
} }
@ -210,12 +225,53 @@ export default class GraphController {
.put(fileSlice); .put(fileSlice);
} }
private saveMailCallback(error: any, response: any, rawResponse?: any): void { public saveMailMetadata(mailId: string, displayName: string, url: string, savedDate: Date) {
if (error !== null) { if (this.saveMetadata) {
console.log(error); const apiUrl = `/me/messages/${mailId}/extensions`;
const metadataBody = {
"@odata.type" : "microsoft.graph.openTypeExtension",
"extensionName" : this.metadataExtensionName,
"saveDisplayName" : displayName,
"saveUrl" : url,
"savedDate" : savedDate.toISOString()
};
this.client
.api(apiUrl)
.version('v1.0')
.post(JSON.stringify(metadataBody))
.then((response) => {
console.log(response);
});
}
}
public retrieveMailMetadata(mailId: string): Promise<any> {
const apiUrl = `/me/messages/${mailId}`;
const expand = `Extensions($filter=id eq 'Microsoft.OutlookServices.OpenTypeExtension.${this.metadataExtensionName}')`;
return this.client
.api(apiUrl)
.version('v1.0')
.expand(expand)
.select('id,subject,extensions')
.get()
.then((response) => {
if (typeof response.extensions !== 'undefined' && response.extensions !== null) {
const metadata: IMailMetadata = {
extensionName: response.extensions[0].extensionName,
saveDisplayName: response.extensions[0].saveDisplayName,
saveUrl: response.extensions[0].saveUrl,
savedDate: new Date(response.extensions[0].savedDate)
};
return metadata;
} }
else { else {
console.log(response); return null;
} }
},
(error) => {
console.log(error);
return null;
});
} }
} }

View File

@ -4,4 +4,5 @@ export interface IFolder {
id: string; id: string;
driveID: string; driveID: string;
parentFolder: IFolder; parentFolder: IFolder;
webUrl: string;
} }

View File

@ -0,0 +1,6 @@
export interface IMailMetadata {
extensionName: string;
saveDisplayName: string;
saveUrl: string;
savedDate: Date;
}

View File

@ -3,7 +3,7 @@ import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library'; import { Version } from '@microsoft/sp-core-library';
import { import {
IPropertyPaneConfiguration, IPropertyPaneConfiguration,
PropertyPaneTextField PropertyPaneToggle
} from '@microsoft/sp-property-pane'; } from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
@ -13,7 +13,7 @@ import Outlook2SharePoint from './components/Outlook2SharePoint';
import { IOutlook2SharePointProps } from './components/IOutlook2SharePointProps'; import { IOutlook2SharePointProps } from './components/IOutlook2SharePointProps';
export interface IOutlook2SharePointWebPartProps { export interface IOutlook2SharePointWebPartProps {
description: string;
} }
export default class Outlook2SharePointWebPart extends BaseClientSideWebPart <IOutlook2SharePointWebPartProps> { export default class Outlook2SharePointWebPart extends BaseClientSideWebPart <IOutlook2SharePointWebPartProps> {
@ -21,8 +21,9 @@ export default class Outlook2SharePointWebPart extends BaseClientSideWebPart <IO
let mail: IMail = null; let mail: IMail = null;
if (this.context.sdks.office) { if (this.context.sdks.office) {
const item = this.context.sdks.office.context.mailbox.item; const item = this.context.sdks.office.context.mailbox.item;
const itemId = this.context.sdks.office.context.mailbox.convertToRestId(item.itemId, 'v2.0');
if (item !== null) { if (item !== null) {
mail = { id: item.itemId,subject: item.subject }; mail = { id: itemId, subject: item.subject };
} }
} }
@ -56,8 +57,8 @@ export default class Outlook2SharePointWebPart extends BaseClientSideWebPart <IO
{ {
groupName: strings.BasicGroupName, groupName: strings.BasicGroupName,
groupFields: [ groupFields: [
PropertyPaneTextField('description', { PropertyPaneToggle('saveMetadata', {
label: strings.DescriptionFieldLabel label: strings.SaveMetadataFieldLabel
}) })
] ]
} }

View File

@ -5,6 +5,7 @@ import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import Breadcrumb from './controls/Breadcrumb'; import Breadcrumb from './controls/Breadcrumb';
import Folder from './Folder'; import Folder from './Folder';
import styles from './Groups.module.scss'; import styles from './Groups.module.scss';
import * as strings from 'Outlook2SharePointWebPartStrings';
import { IGroupsProps } from './IGroupsProps'; import { IGroupsProps } from './IGroupsProps';
import { IGroupsState } from './IGroupsState'; import { IGroupsState } from './IGroupsState';
import { IFolder } from '../../../model/IFolder'; import { IFolder } from '../../../model/IFolder';
@ -16,6 +17,7 @@ export default class Groups extends React.Component<IGroupsProps, IGroupsState>
folders: [], folders: [],
grandParentFolder: null, grandParentFolder: null,
parentFolder: null, parentFolder: null,
selectedGroupName: '',
showSpinner: false showSpinner: false
}; };
} }
@ -50,7 +52,7 @@ export default class Groups extends React.Component<IGroupsProps, IGroupsState>
<div> <div>
<PrimaryButton <PrimaryButton
className={styles.saveBtn} className={styles.saveBtn}
text="Save here" text={strings.SaveLabel}
onClick={this.saveMailTo} onClick={this.saveMailTo}
disabled={this.state.parentFolder === null} disabled={this.state.parentFolder === null}
allowDisabledFocus={true} allowDisabledFocus={true}
@ -58,7 +60,7 @@ export default class Groups extends React.Component<IGroupsProps, IGroupsState>
{ this.state.showSpinner && ( { this.state.showSpinner && (
<div className={styles.spinnerContainer}> <div className={styles.spinnerContainer}>
<Overlay > <Overlay >
<Spinner size={ SpinnerSize.large } label='Processing request' /> <Spinner size={ SpinnerSize.large } label={strings.SpinnerLabel} />
</Overlay> </Overlay>
</div> </div>
) } ) }
@ -78,19 +80,14 @@ export default class Groups extends React.Component<IGroupsProps, IGroupsState>
} }
private getGroupDrives = (group: IFolder) => { 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) => { this.props.graphController.getGroupDrives(group).then((folders) => {
if (folders.length > 0) { if (folders.length > 0) {
this.setState((prevState: IGroupsState, props: IGroupsProps) => { this.setState((prevState: IGroupsState, props: IGroupsProps) => {
return { return {
folders: folders, folders: folders,
grandParentFolder: null, grandParentFolder: null,
parentFolder: group parentFolder: group,
selectedGroupName: group.name
}; };
}); });
} }
@ -122,7 +119,6 @@ export default class Groups extends React.Component<IGroupsProps, IGroupsState>
} }
} }
private showRoot = () => { private showRoot = () => {
this.getGroups(); this.getGroups();
} }
@ -142,7 +138,11 @@ export default class Groups extends React.Component<IGroupsProps, IGroupsState>
showSpinner: true showSpinner: true
}; };
}); });
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback); this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback)
.then((response: string) => {
const saveLocationDisplayName = `${this.state.selectedGroupName} ...> ${this.state.grandParentFolder.name} > ${this.state.parentFolder.name}`;
this.props.graphController.saveMailMetadata(this.props.mail.id, saveLocationDisplayName, this.state.parentFolder.webUrl, new Date());
});
} }
private saveMailCallback = (message: string) => { private saveMailCallback = (message: string) => {
@ -152,10 +152,10 @@ export default class Groups extends React.Component<IGroupsProps, IGroupsState>
}; };
}); });
if (message.indexOf('Success') > -1) { if (message.indexOf('Success') > -1) {
this.props.successCallback(message); this.props.successCallback(strings.SuccessMessage);
} }
else { else {
this.props.errorCallback(message); this.props.errorCallback(strings.ErrorMessage);
} }
} }
} }

View File

@ -4,5 +4,6 @@ export interface IGroupsState {
folders: IFolder[]; folders: IFolder[];
grandParentFolder: IFolder; grandParentFolder: IFolder;
parentFolder: IFolder; parentFolder: IFolder;
selectedGroupName: string;
showSpinner: boolean; showSpinner: boolean;
} }

View File

@ -1,7 +1,9 @@
import GraphController from '../../../controller/GraphController'; import GraphController from '../../../controller/GraphController';
import { IMailMetadata } from '../../../model/IMailMetadata';
export interface IOutlook2SharePointState { export interface IOutlook2SharePointState {
graphController: GraphController; graphController: GraphController;
mailMetadata: IMailMetadata;
showSuccess: boolean; showSuccess: boolean;
showError: boolean; showError: boolean;
showOneDrive: boolean; showOneDrive: boolean;

View File

@ -4,5 +4,6 @@ export interface ITeamsState {
folders: IFolder[]; folders: IFolder[];
grandParentFolder: IFolder; grandParentFolder: IFolder;
parentFolder: IFolder; parentFolder: IFolder;
selectedTeamName: string;
showSpinner: boolean; showSpinner: boolean;
} }

View File

@ -4,6 +4,7 @@ import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import Folder from './Folder'; import Folder from './Folder';
import styles from './Groups.module.scss'; import styles from './Groups.module.scss';
import * as strings from 'Outlook2SharePointWebPartStrings';
import Breadcrumb from './controls/Breadcrumb'; import Breadcrumb from './controls/Breadcrumb';
import { IOneDriveProps } from './IOneDriveProps'; import { IOneDriveProps } from './IOneDriveProps';
import { IOneDriveState } from './IOneDriveState'; import { IOneDriveState } from './IOneDriveState';
@ -49,14 +50,14 @@ export default class OneDrive extends React.Component<IOneDriveProps, IOneDriveS
<div> <div>
<PrimaryButton <PrimaryButton
className={styles.saveBtn} className={styles.saveBtn}
text="Save here" text={strings.SaveLabel}
onClick={this.saveMailTo} onClick={this.saveMailTo}
allowDisabledFocus={true} allowDisabledFocus={true}
/> />
{ this.state.showSpinner && ( { this.state.showSpinner && (
<div className={styles.spinnerContainer}> <div className={styles.spinnerContainer}>
<Overlay > <Overlay >
<Spinner size={ SpinnerSize.large } label='Processing request' /> <Spinner size={ SpinnerSize.large } label={strings.SpinnerLabel} />
</Overlay> </Overlay>
</div> </div>
) } ) }
@ -103,7 +104,11 @@ export default class OneDrive extends React.Component<IOneDriveProps, IOneDriveS
showSpinner: true showSpinner: true
}; };
}); });
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback); this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback)
.then((response: string) => {
const saveLocationDisplayName = `OneDrive ...> ${this.state.grandParentFolder.name} > ${this.state.parentFolder.name}`;
this.props.graphController.saveMailMetadata(this.props.mail.id, saveLocationDisplayName, this.state.parentFolder.webUrl, new Date());
});
} }
private saveMailCallback = (message: string) => { private saveMailCallback = (message: string) => {
@ -113,10 +118,10 @@ export default class OneDrive extends React.Component<IOneDriveProps, IOneDriveS
}; };
}); });
if (message.indexOf('Success') > -1) { if (message.indexOf('Success') > -1) {
this.props.successCallback(message); this.props.successCallback(strings.SuccessMessage);
} }
else { else {
this.props.errorCallback(message); this.props.errorCallback(strings.ErrorMessage);
} }
} }
} }

View File

@ -14,4 +14,19 @@
@include ms-fontSize-l; @include ms-fontSize-l;
margin-left: 3px; margin-left: 3px;
} }
.metadata {
margin-left: 6px;
margin-bottom: 8px;
}
.subMetadata {
margin-left: 6px;
a {
cursor: pointer;
text-decoration: none;
color: inherit;
}
a:hover {
@include ms-fontColor-themePrimary;
}
}
} }

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import styles from './Outlook2SharePoint.module.scss'; import styles from './Outlook2SharePoint.module.scss';
import * as strings from 'Outlook2SharePointWebPartStrings';
import { Icon } from 'office-ui-fabric-react/lib/Icon'; import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import GraphController from '../../../controller/GraphController'; import GraphController from '../../../controller/GraphController';
@ -11,11 +12,13 @@ import { IOutlook2SharePointState } from './IOutlook2SharePointState';
export default class Outlook2SharePoint extends React.Component<IOutlook2SharePointProps, IOutlook2SharePointState> { export default class Outlook2SharePoint extends React.Component<IOutlook2SharePointProps, IOutlook2SharePointState> {
private graphController: GraphController; private graphController: GraphController;
private saveMetadata = true; // For simplicity reasons and as I am not convinced with the current "Property handling" of Office Add-In we configure 'hard-coded'
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
graphController: null, graphController: null,
mailMetadata: null,
showError: false, showError: false,
showSuccess: false, showSuccess: false,
showOneDrive: false, showOneDrive: false,
@ -24,13 +27,25 @@ export default class Outlook2SharePoint extends React.Component<IOutlook2SharePo
successMessage: '', successMessage: '',
errorMessage: '' errorMessage: ''
}; };
this.graphController = new GraphController(this.props.msGraphClientFactory, this.graphClientReadyCallback); this.graphController = new GraphController(this.saveMetadata);
this.graphController.init(this.props.msGraphClientFactory)
.then((controllerReady) => {
if (controllerReady) {
this.graphClientReady();
}
});
} }
public render(): React.ReactElement<IOutlook2SharePointProps> { public render(): React.ReactElement<IOutlook2SharePointProps> {
return ( return (
<div className={ styles.outlook2SharePoint }> <div className={ styles.outlook2SharePoint }>
{this.state.mailMetadata !== null &&
<div className={styles.metadata}>
<div><Icon iconName="InfoSolid" /> {strings.SaveInfo}</div>
<div className={styles.subMetadata}>{strings.To} <a href={this.state.mailMetadata.saveUrl}>{this.state.mailMetadata.saveDisplayName}</a></div>
<div className={styles.subMetadata}>{strings.On} <span>{this.state.mailMetadata.savedDate.toLocaleDateString()}</span></div>
</div>}
{this.state.showSuccess && <div> {this.state.showSuccess && <div>
<MessageBar <MessageBar
messageBarType={MessageBarType.success} messageBarType={MessageBarType.success}
@ -97,12 +112,15 @@ export default class Outlook2SharePoint extends React.Component<IOutlook2SharePo
/** /**
* This function first retrieves all OneDrive root folders from user * This function first retrieves all OneDrive root folders from user
*/ */
private graphClientReadyCallback = () => { private graphClientReady = () => {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => { this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return { return {
graphController: this.graphController graphController: this.graphController
}; };
}); });
if (this.saveMetadata) {
this.getMetadata();
}
} }
private showError = (message: string) => { private showError = (message: string) => {
@ -172,4 +190,17 @@ export default class Outlook2SharePoint extends React.Component<IOutlook2SharePo
}; };
}); });
} }
private getMetadata() {
this.state.graphController.retrieveMailMetadata(this.props.mail.id)
.then((response) => {
if (response !== null) {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return {
mailMetadata: response
};
});
}
});
}
} }

View File

@ -5,6 +5,7 @@ import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import Breadcrumb from './controls/Breadcrumb'; import Breadcrumb from './controls/Breadcrumb';
import Folder from './Folder'; import Folder from './Folder';
import styles from './Groups.module.scss'; import styles from './Groups.module.scss';
import * as strings from 'Outlook2SharePointWebPartStrings';
import { ITeamsProps } from './ITeamsProps'; import { ITeamsProps } from './ITeamsProps';
import { ITeamsState } from './ITeamsState'; import { ITeamsState } from './ITeamsState';
import { IFolder } from '../../../model/IFolder'; import { IFolder } from '../../../model/IFolder';
@ -16,6 +17,7 @@ export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
folders: [], folders: [],
grandParentFolder: null, grandParentFolder: null,
parentFolder: null, parentFolder: null,
selectedTeamName: '',
showSpinner: false showSpinner: false
}; };
} }
@ -50,7 +52,7 @@ export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
<div> <div>
<PrimaryButton <PrimaryButton
className={styles.saveBtn} className={styles.saveBtn}
text="Save here" text={strings.SaveLabel}
onClick={this.saveMailTo} onClick={this.saveMailTo}
disabled={this.state.parentFolder === null} disabled={this.state.parentFolder === null}
allowDisabledFocus={true} allowDisabledFocus={true}
@ -58,7 +60,7 @@ export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
{ this.state.showSpinner && ( { this.state.showSpinner && (
<div className={styles.spinnerContainer}> <div className={styles.spinnerContainer}>
<Overlay > <Overlay >
<Spinner size={ SpinnerSize.large } label='Processing request' /> <Spinner size={ SpinnerSize.large } label={strings.SpinnerLabel} />
</Overlay> </Overlay>
</div> </div>
) } ) }
@ -78,19 +80,14 @@ export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
} }
private getGroupDrives = (group: IFolder) => { 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) => { this.props.graphController.getGroupDrives(group).then((folders) => {
if (folders.length > 0) { if (folders.length > 0) {
this.setState((prevState: ITeamsState, props: ITeamsProps) => { this.setState((prevState: ITeamsState, props: ITeamsProps) => {
return { return {
folders: folders, folders: folders,
grandParentFolder: null, grandParentFolder: null,
parentFolder: group parentFolder: group,
selectedTeamName: group.name
}; };
}); });
} }
@ -122,7 +119,6 @@ export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
} }
} }
private showRoot = () => { private showRoot = () => {
this.getTeams(); this.getTeams();
} }
@ -142,7 +138,11 @@ export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
showSpinner: true showSpinner: true
}; };
}); });
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback); this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback)
.then((response: string) => {
const saveLocationDisplayName = `${this.state.selectedTeamName} ...> ${this.state.grandParentFolder.name} > ${this.state.parentFolder.name}`;
this.props.graphController.saveMailMetadata(this.props.mail.id, saveLocationDisplayName, this.state.parentFolder.webUrl, new Date());
});
} }
private saveMailCallback = (message: string) => { private saveMailCallback = (message: string) => {
@ -152,10 +152,10 @@ export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
}; };
}); });
if (message.indexOf('Success') > -1) { if (message.indexOf('Success') > -1) {
this.props.successCallback(message); this.props.successCallback(strings.SuccessMessage);
} }
else { else {
this.props.errorCallback(message); this.props.errorCallback(strings.ErrorMessage);
} }
} }
} }

View File

@ -1,7 +1,14 @@
define([], function() { define([], function() {
return { return {
"PropertyPaneDescription": "Description", "PropertyPaneDescription": "Copy to OneDrive/Teams",
"BasicGroupName": "Group Name", "BasicGroupName": "Configuration",
"DescriptionFieldLabel": "Description Field" "SaveMetadataFieldLabel": "Save Metadata on copied mail",
"SaveInfo": "You already saved this mail",
"To": "to",
"On": "on",
"SaveLabel": "Save here",
"SpinnerLabel": "Processing request",
"SuccessMessage": "Success",
"ErrorMessage": "Error occured"
} }
}); });

View File

@ -1,7 +1,14 @@
declare interface IOutlook2SharePointWebPartStrings { declare interface IOutlook2SharePointWebPartStrings {
PropertyPaneDescription: string; PropertyPaneDescription: string;
BasicGroupName: string; BasicGroupName: string;
DescriptionFieldLabel: string; SaveMetadataFieldLabel: string;
SaveInfo: string;
To: string;
On: string;
SaveLabel: string;
SpinnerLabel: string;
SuccessMessage: string;
ErrorMessage: string;
} }
declare module 'Outlook2SharePointWebPartStrings' { declare module 'Outlook2SharePointWebPartStrings' {