diff --git a/src/client/app/actions/snippet.ts b/src/client/app/actions/snippet.ts
index f2f48964..cada29f4 100644
--- a/src/client/app/actions/snippet.ts
+++ b/src/client/app/actions/snippet.ts
@@ -72,7 +72,7 @@ export class ImportSuccessAction implements Action {
export class UpdateInfoAction implements Action {
readonly type = SnippetActionTypes.UPDATE_INFO;
- constructor(public payload: { id: string, name?: string, description?: string, gist?: string, gistOwnerId?: string }) { }
+ constructor(public payload: { id: string, name?: string, description?: string, gist?: string, gistOwnerId?: string, endpoints?: string[] }) { }
}
export class RunAction implements Action {
diff --git a/src/client/app/components/snippet.info.ts b/src/client/app/components/snippet.info.ts
index 3ae26b1e..440fc4e1 100644
--- a/src/client/app/components/snippet.info.ts
+++ b/src/client/app/components/snippet.info.ts
@@ -1,5 +1,5 @@
import { Component, Input, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core';
-import { getGistUrl, environment, storage } from '../helpers';
+import { getGistUrl, environment, storage, outlookEndpoints } from '../helpers';
import { Strings } from '../strings';
import { isNil } from 'lodash';
@@ -19,19 +19,51 @@ import { isNil } from 'lodash';
+
+
+
+
+
-
+
{{strings.gistUrlLinkLabel}}
+
-
@@ -45,6 +77,13 @@ export class SnippetInfo {
strings = Strings();
+ get buttonClasses() {
+ return {
+ 'ms-Button ms-Button--primary': true,
+ 'is-disabled': this.saveDisabled,
+ };
+ }
+
get showGistUrl() {
if (!this.snippet.gist) {
return false;
@@ -67,4 +106,93 @@ export class SnippetInfo {
let host = this.snippet.host.toLowerCase();
return `${environment.current.config.editorUrl}/#/view/${host}/gist/${this.snippet.gist}`;
}
+
+ // Outlook Specific tooling
+
+ get inOutlook() {
+ return this.snippet.host.toLowerCase() === 'outlook';
+ }
+
+ get MailRead() {
+ if (this.snippet.endpoints === undefined) {
+ return false;
+ }
+ return this.snippet.endpoints.indexOf(outlookEndpoints.MailRead) !== -1;
+ }
+
+ @Input()
+ set MailRead(checked: boolean) {
+ this.snippet.endpoints = this.snippet.endpoints ? this.snippet.endpoints : [];
+ if (checked) {
+ if (this.snippet.endpoints.indexOf(outlookEndpoints.MailRead) === -1) {
+ this.snippet.endpoints.push(outlookEndpoints.MailRead);
+ }
+ } else {
+ this.snippet.endpoints = this.snippet.endpoints.filter(endpoint => endpoint !== outlookEndpoints.MailRead);
+ }
+ }
+
+ get MailCompose() {
+ if (this.snippet.endpoints === undefined) {
+ return false;
+ }
+ return this.snippet.endpoints.indexOf(outlookEndpoints.MailCompose) !== -1;
+ }
+
+ @Input()
+ set MailCompose(checked: boolean) {
+ this.snippet.endpoints = this.snippet.endpoints ? this.snippet.endpoints : [];
+ if (checked) {
+ if (this.snippet.endpoints.indexOf(outlookEndpoints.MailCompose) === -1) {
+ this.snippet.endpoints.push(outlookEndpoints.MailCompose);
+ }
+ } else {
+ this.snippet.endpoints = this.snippet.endpoints.filter(endpoint => endpoint !== outlookEndpoints.MailCompose);
+ }
+ }
+
+ get AppointmentOrganizer() {
+ if (this.snippet.endpoints === undefined) {
+ return false;
+ }
+ return this.snippet.endpoints.indexOf(outlookEndpoints.AppointmentOrganizer) !== -1;
+ }
+
+ @Input()
+ set AppointmentOrganizer(checked: boolean) {
+ this.snippet.endpoints = this.snippet.endpoints ? this.snippet.endpoints : [];
+ if (checked) {
+ if (this.snippet.endpoints.indexOf(outlookEndpoints.AppointmentOrganizer) === -1) {
+ this.snippet.endpoints.push(outlookEndpoints.AppointmentOrganizer);
+ }
+ } else {
+ this.snippet.endpoints = this.snippet.endpoints.filter(endpoint => endpoint !== outlookEndpoints.AppointmentOrganizer);
+ }
+ }
+
+ get AppointmentAttendee() {
+ if (this.snippet.endpoints === undefined) {
+ return false;
+ }
+ return this.snippet.endpoints.indexOf(outlookEndpoints.AppointmentAttendee) !== -1;
+ }
+
+ @Input()
+ set AppointmentAttendee(checked: boolean) {
+ this.snippet.endpoints = this.snippet.endpoints ? this.snippet.endpoints : [];
+ if (checked) {
+ if (this.snippet.endpoints.indexOf(outlookEndpoints.AppointmentAttendee) === -1) {
+ this.snippet.endpoints.push(outlookEndpoints.AppointmentAttendee);
+ }
+ } else {
+ this.snippet.endpoints = this.snippet.endpoints.filter(endpoint => endpoint !== outlookEndpoints.AppointmentAttendee);
+ }
+ }
+
+ @Input()
+ get saveDisabled() {
+ // In outlook, at least one endpoint must be enabled, so we disable the save button unless at least one is checked.
+ return this.inOutlook && !(this.MailRead || this.MailCompose || this.AppointmentAttendee || this.AppointmentOrganizer);
+ }
+
}
diff --git a/src/client/app/containers/editor.mode.ts b/src/client/app/containers/editor.mode.ts
index 0867c3a0..eefa43c3 100644
--- a/src/client/app/containers/editor.mode.ts
+++ b/src/client/app/containers/editor.mode.ts
@@ -44,7 +44,7 @@ import { Subscription } from 'rxjs/Subscription';
-
+
@@ -56,6 +56,7 @@ export class EditorMode {
snippet: ISnippet;
isEmpty: boolean;
isDisabled: boolean;
+ showInfo: boolean;
strings = Strings();
@@ -63,6 +64,7 @@ export class EditorMode {
private sharingSub: Subscription;
private errorsSub: Subscription;
+
constructor(
private _store: Store,
private _effects: UIEffects,
@@ -72,6 +74,11 @@ export class EditorMode {
this.snippetSub = this._store.select(fromRoot.getCurrent).subscribe(snippet => {
this.isEmpty = snippet == null;
this.snippet = snippet;
+ const inOutlook = this.snippet !== null && this.snippet.host.toLowerCase() === 'outlook';
+ const outlookNeedsEndpoints = inOutlook && (this.snippet.endpoints === undefined || this.snippet.endpoints.length === 0);
+ if (outlookNeedsEndpoints) {
+ this.showInfo = true;
+ }
});
this.sharingSub = this._store.select(fromRoot.getSharing).subscribe(sharing => {
@@ -83,6 +90,10 @@ export class EditorMode {
this.parseEditorRoutingParams();
}
+ get shouldShowInfo() {
+ return this.showInfo;
+ }
+
get isAddinCommands() {
return environment.current.isAddinCommands;
}
diff --git a/src/client/app/effects/snippet.ts b/src/client/app/effects/snippet.ts
index d01341f5..68938e74 100644
--- a/src/client/app/effects/snippet.ts
+++ b/src/client/app/effects/snippet.ts
@@ -18,6 +18,14 @@ import { isEmpty, isNil, find, assign, reduce, forIn, isEqual } from 'lodash';
import * as sha1 from 'crypto-js/sha1';
import { Utilities, HostType } from '@microsoft/office-js-helpers';
+function playlistUrl() {
+ let host = environment.current.host.toLowerCase();
+ if (environment.current.endpoint !== null) {
+ host += `-${environment.current.endpoint}`;
+ }
+ return `${environment.current.config.samplesUrl}/playlists/${host}.yaml`;
+}
+
@Injectable()
export class SnippetEffects {
constructor(
@@ -173,7 +181,7 @@ export class SnippetEffects {
.map((action: Snippet.LoadTemplatesAction) => action.payload)
.mergeMap(source => {
if (source === 'LOCAL') {
- let snippetJsonUrl = `${environment.current.config.samplesUrl}/playlists/${environment.current.host.toLowerCase()}.yaml`;
+ let snippetJsonUrl = playlistUrl();
return this._request.get(snippetJsonUrl, ResponseTypes.YAML);
}
else {
@@ -192,7 +200,7 @@ export class SnippetEffects {
updateInfo$: Observable = this.actions$
.ofType(Snippet.SnippetActionTypes.UPDATE_INFO)
.map(({ payload }) => {
- let { id, name, description, gist, gistOwnerId } = payload;
+ let { id, name, description, gist, gistOwnerId, endpoints } = payload;
let snippet: ISnippet = storage.lastOpened;
if (storage.snippets.contains(id)) {
snippet = storage.snippets.get(id);
@@ -210,6 +218,9 @@ export class SnippetEffects {
if (!isNil(gistOwnerId)) {
snippet.gistOwnerId = gistOwnerId;
}
+ if (!isNil(endpoints)) {
+ snippet.endpoints = endpoints;
+ }
/* updates snippet */
storage.snippets.insert(id, snippet);
diff --git a/src/client/app/helpers/environment.ts b/src/client/app/helpers/environment.ts
index 5ea45529..853889a8 100644
--- a/src/client/app/helpers/environment.ts
+++ b/src/client/app/helpers/environment.ts
@@ -64,6 +64,7 @@ class Environment {
host: null,
platform: null,
+ endpoint: null,
runtimeSessionTimestamp: (new Date()).getTime().toString()
};
@@ -164,6 +165,7 @@ class Environment {
commands: any/* whether app-commands are available, relevant for Office Add-ins */,
mode: string /* and older way of opening Script Lab to a particular host */,
host: string /* same as "mode", also needed here so that overrides can also have this parameter */,
+ endpoint: string /* Defines which type of outlook experience is active */,
wacUrl: string,
tryIt: any,
};
@@ -200,6 +202,9 @@ class Environment {
}
if (isValidHost(pageParams.mode)) {
this.appendCurrent({ host: pageParams.mode.toUpperCase() });
+ if (pageParams.endpoint) {
+ this.appendCurrent({endpoint: pageParams.endpoint.toLowerCase()});
+ }
return true;
}
}
diff --git a/src/client/app/helpers/utilities.ts b/src/client/app/helpers/utilities.ts
index 3e0e0a7f..66ebe83f 100644
--- a/src/client/app/helpers/utilities.ts
+++ b/src/client/app/helpers/utilities.ts
@@ -15,6 +15,13 @@ const officeHostsToAppNames = {
'WORD': 'Word'
};
+export const outlookEndpoints = {
+ MailRead: 'messageread',
+ MailCompose: 'messagecompose',
+ AppointmentOrganizer: 'appointmentcompose',
+ AppointmentAttendee: 'appointmentread',
+};
+
export function isValidHost(host: string) {
host = host.toUpperCase();
return isOfficeHost(host) || (host === 'WEB');
diff --git a/src/client/app/strings/chinese-simplified.ts b/src/client/app/strings/chinese-simplified.ts
index 16c34160..772dfcdb 100644
--- a/src/client/app/strings/chinese-simplified.ts
+++ b/src/client/app/strings/chinese-simplified.ts
@@ -192,6 +192,11 @@ export function getChineseSimplifiedStrings(): ClientStringsPerLanguage {
// Outlook-only strings
noRunInOutlook: getEnglishSubstitutesForNotYetTranslated().noRunInOutlook,
+ extensionPointsLabel: getEnglishSubstitutesForNotYetTranslated().extensionPointsLabel,
+ mailRead: getEnglishSubstitutesForNotYetTranslated().mailRead,
+ mailCompose: getEnglishSubstitutesForNotYetTranslated().mailCompose,
+ appointmentOrganizer: getEnglishSubstitutesForNotYetTranslated().appointmentOrganizer,
+ appointmentAttendee: getEnglishSubstitutesForNotYetTranslated().appointmentAttendee,
// import.ts strings
diff --git a/src/client/app/strings/english.ts b/src/client/app/strings/english.ts
index 3e2ce697..9855b2d7 100644
--- a/src/client/app/strings/english.ts
+++ b/src/client/app/strings/english.ts
@@ -195,6 +195,11 @@ export function getEnglishStrings(): ClientStringsPerLanguage {
// Outlook-only strings
noRunInOutlook: /** NEEDS STRING REVIEW **/ `You cannot run your snippet from the code window in Outlook. Please open the "Run" pane in Outlook to run your snippet.`,
+ extensionPointsLabel: /** NEEDS STRING REVIEW **/ `Supported Extension Points`,
+ mailRead: /** NEEDS STRING REVIEW **/ `Mail Read`,
+ mailCompose: /** NEEDS STRING REVIEW **/ `Mail Compose`,
+ appointmentOrganizer: /** NEEDS STRING REVIEW **/ `Appointment Organizer`,
+ appointmentAttendee: /** NEEDS STRING REVIEW **/ `Appointment Attendee`,
// import.ts strings
diff --git a/src/client/app/strings/german.ts b/src/client/app/strings/german.ts
index f986f93f..9028dfd3 100644
--- a/src/client/app/strings/german.ts
+++ b/src/client/app/strings/german.ts
@@ -194,6 +194,11 @@ export function getGermanStrings(): ClientStringsPerLanguage {
// Outlook-only strings
noRunInOutlook: 'Das Code-Schnipsel kann in Outlook nicht aus dem Code-Fenster heraus ausgeführt werden. Bitte öffnen Sie den Aufgabenbereich zur Code-Ausführung und rufen Sie das Schnipsel von dort aus auf.',
+ extensionPointsLabel: getEnglishSubstitutesForNotYetTranslated().extensionPointsLabel,
+ mailRead: getEnglishSubstitutesForNotYetTranslated().mailRead,
+ mailCompose: getEnglishSubstitutesForNotYetTranslated().mailCompose,
+ appointmentOrganizer: getEnglishSubstitutesForNotYetTranslated().appointmentOrganizer,
+ appointmentAttendee: getEnglishSubstitutesForNotYetTranslated().appointmentAttendee,
// import.ts strings
diff --git a/src/client/app/strings/spanish.ts b/src/client/app/strings/spanish.ts
index 24b40d3f..4519b384 100644
--- a/src/client/app/strings/spanish.ts
+++ b/src/client/app/strings/spanish.ts
@@ -189,6 +189,11 @@ export function getSpanishStrings(): ClientStringsPerLanguage {
// Outlook-only strings
noRunInOutlook: getEnglishSubstitutesForNotYetTranslated().noRunInOutlook,
+ extensionPointsLabel: getEnglishSubstitutesForNotYetTranslated().extensionPointsLabel,
+ mailRead: getEnglishSubstitutesForNotYetTranslated().mailRead,
+ mailCompose: getEnglishSubstitutesForNotYetTranslated().mailCompose,
+ appointmentOrganizer: getEnglishSubstitutesForNotYetTranslated().appointmentOrganizer,
+ appointmentAttendee: getEnglishSubstitutesForNotYetTranslated().appointmentAttendee,
// import.ts strings
diff --git a/src/client/assets/styles/common.scss b/src/client/assets/styles/common.scss
index 71bce6b8..56c19ab6 100644
--- a/src/client/assets/styles/common.scss
+++ b/src/client/assets/styles/common.scss
@@ -2,6 +2,7 @@
@import 'mixins';
@import 'components/spinner';
@import 'components/command';
+@import 'components/checkbox';
* {
margin: 0;
diff --git a/src/client/assets/styles/components/checkbox.scss b/src/client/assets/styles/components/checkbox.scss
new file mode 100644
index 00000000..0a79b97e
--- /dev/null
+++ b/src/client/assets/styles/components/checkbox.scss
@@ -0,0 +1,66 @@
+ /* Customize the label (the container) */
+ .container {
+ display: block;
+ position: relative;
+ padding-left: 35px;
+ margin-bottom: 12px;
+ cursor: pointer;
+ font-size: 22px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ height: 25px;
+ }
+
+ /* Hide the browser's default checkbox */
+ .container input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ }
+
+ /* Create a custom checkbox */
+ .checkmark {
+ position: absolute;
+ top: 5px;
+ left: 0;
+ height: 20px;
+ width: 20px;
+ background-color: #eee;
+ }
+
+ /* On mouse-over, add a grey background color */
+ .container:hover input ~ .checkmark {
+ background-color: #ccc;
+ }
+
+ /* When the checkbox is checked, add a blue background */
+ .container input:checked ~ .checkmark {
+ background-color: #2196F3;
+ }
+
+ /* Create the checkmark/indicator (hidden when not checked) */
+ .checkmark:after {
+ content: "";
+ position: absolute;
+ display: none;
+ }
+
+ /* Show the checkmark when checked */
+ .container input:checked ~ .checkmark:after {
+ display: block;
+ }
+
+ /* Style the checkmark/indicator */
+ .container .checkmark:after {
+ left: 9px;
+ top: 5px;
+ width: 5px;
+ height: 10px;
+ border: solid white;
+ border-width: 0 3px 3px 0;
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+ }
\ No newline at end of file
diff --git a/src/client/public/functions.ts b/src/client/public/functions.ts
index 042c0780..cb9ac166 100644
--- a/src/client/public/functions.ts
+++ b/src/client/public/functions.ts
@@ -3,7 +3,11 @@ const { safeExternalUrls } = PLAYGROUND;
Office.initialize = () => {
const tutorialUrl = `${window.location.origin}/tutorial.html`;
- const codeUrl = `${window.location.origin}/?mode=${Utilities.host}`;
+ function codeUrl() {
+ const item = Office.context.mailbox.item as Office.MessageRead;
+ const endpoint = `${item.itemType}${item.itemId !== undefined ? 'read' : 'compose'}`;
+ return `${window.location.origin}/?mode=${Utilities.host}&endpoint=${endpoint}`;
+ }
const launchInDialog = (url: string, event?: any, options?: { width?: number, height?: number, displayInIframe?: boolean }) => {
options = options || {};
@@ -34,7 +38,7 @@ Office.initialize = () => {
launchInDialog(`${window.location.origin}/external-page.html?destination=${encodeURIComponent(url)}`, event, options);
};
- (window as any).launchCode = (event) => launchInDialog(codeUrl, event, { width: 75, height: 75, displayInIframe: false });
+ (window as any).launchCode = (event) => launchInDialog(codeUrl(), event, { width: 75, height: 75, displayInIframe: false });
(window as any).launchTutorial = (event) => launchInDialog(tutorialUrl, event, { width: 35, height: 45 });
diff --git a/src/interfaces/client-strings.ts b/src/interfaces/client-strings.ts
index d7761b1e..838c4b5f 100644
--- a/src/interfaces/client-strings.ts
+++ b/src/interfaces/client-strings.ts
@@ -171,6 +171,11 @@ interface ClientStringsPerLanguage {
// Outlook-only strings
noRunInOutlook: string;
+ extensionPointsLabel: string;
+ mailRead: string;
+ mailCompose: string;
+ appointmentOrganizer: string;
+ appointmentAttendee: string;
// import.ts strings
diff --git a/src/interfaces/playground.d.ts b/src/interfaces/playground.d.ts
index 5a624ec6..eb7b85d7 100644
--- a/src/interfaces/playground.d.ts
+++ b/src/interfaces/playground.d.ts
@@ -16,6 +16,7 @@ interface ITemplate {
/** author: export-only */
author?: string;
host: string;
+ endpoints?: string[];
/** api_set: export-only (+ check at first level of import) */
api_set?: {
[index: string]: number
@@ -211,6 +212,7 @@ interface ICurrentPlaygroundInfo {
config: Readonly;
host: Readonly;
platform: Readonly;
+ endpoint: Readonly;
/** A timestamp specifically for the in-memory session (i.e.,
* even more short-term than sessionStorage, which has a lifetime-of-tab duration;