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;