Open extensions added to store metadata to saved mail
This commit is contained in:
parent
e4e1a044b1
commit
6fd6866903
|
@ -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" />
|
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resource": "Microsoft Graph",
|
"resource": "Microsoft Graph",
|
||||||
"scope": "Mail.Read"
|
"scope": "Mail.ReadWrite"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resource": "Microsoft Graph",
|
"resource": "Microsoft Graph",
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,5 @@ export interface IFolder {
|
||||||
id: string;
|
id: string;
|
||||||
driveID: string;
|
driveID: string;
|
||||||
parentFolder: IFolder;
|
parentFolder: IFolder;
|
||||||
|
webUrl: string;
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface IMailMetadata {
|
||||||
|
extensionName: string;
|
||||||
|
saveDisplayName: string;
|
||||||
|
saveUrl: string;
|
||||||
|
savedDate: Date;
|
||||||
|
}
|
|
@ -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
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -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' {
|
||||||
|
|
Loading…
Reference in New Issue