diff --git a/product.json b/product.json index ddc7859aae..f1332004f5 100644 --- a/product.json +++ b/product.json @@ -16,6 +16,8 @@ "darwinBundleIdentifier": "com.sqlopsstudio.oss", "reportIssueUrl": "https://github.com/Microsoft/sqlopsstudio/issues/new?labels=customer%20reported%20issue", "requestFeatureUrl": "https://github.com/Microsoft/sqlopsstudio/issues/new?labels=feature-request", + "privacyStatementUrl": "https://privacy.microsoft.com/en-us/privacystatement", + "telemetryOptOutUrl": "https://github.com/Microsoft/sqlopsstudio/wiki/How-to-Disable-Telemetry-Reporting", "urlProtocol": "sqlops", "enableTelemetry": true, "aiConfig": { diff --git a/src/sql/workbench/common/actions.contribution.ts b/src/sql/workbench/common/actions.contribution.ts index ce6708b0d2..b5a1ba10c4 100644 --- a/src/sql/workbench/common/actions.contribution.ts +++ b/src/sql/workbench/common/actions.contribution.ts @@ -12,7 +12,7 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { ShowCurrentReleaseNotesAction, ProductContribution } from 'sql/workbench/update/releaseNotes'; +import { ShowCurrentReleaseNotesAction } from 'sql/workbench/update/releaseNotes'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; new Actions.BackupAction().registerTask(); @@ -21,8 +21,5 @@ new Actions.NewQueryAction().registerTask(); new Actions.ConfigureDashboardAction().registerTask(); // add product update and release notes contributions -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(ProductContribution, LifecyclePhase.Running); - Registry.as(ActionExtensions.WorkbenchActions) .registerWorkbenchAction(new SyncActionDescriptor(ShowCurrentReleaseNotesAction, ShowCurrentReleaseNotesAction.ID, ShowCurrentReleaseNotesAction.LABEL), 'Show Getting Started'); diff --git a/src/sql/workbench/update/releaseNotes.ts b/src/sql/workbench/update/releaseNotes.ts index 99b57ac2a7..bcb9ab6d12 100644 --- a/src/sql/workbench/update/releaseNotes.ts +++ b/src/sql/workbench/update/releaseNotes.ts @@ -49,39 +49,3 @@ export class ShowCurrentReleaseNotesAction extends AbstractShowReleaseNotesActio super(id, label, pkg.version, editorService, instantiationService); } } - -export class ProductContribution implements IWorkbenchContribution { - - private static KEY = 'releaseNotes/carbonLastVersion'; - getId() { return 'carbon.product'; } - - constructor( - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService, - @INotificationService notificationService: INotificationService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService - ) { - const lastVersion = storageService.get(ProductContribution.KEY, StorageScope.GLOBAL, ''); - - // was there an update? if so, open release notes - if (product.releaseNotesUrl && pkg.version !== lastVersion) { - instantiationService.invokeFunction(loadReleaseNotes, pkg.version).then( - text => editorService.openEditor(instantiationService.createInstance(ReleaseNotesInput, pkg.version, text), { pinned: true }), - () => { - const actions: INotificationActions = { - primary: [ - instantiationService.createInstance(OpenGettingStartedInBrowserAction) - ] - }; - - notificationService.notify({ - severity: Severity.Info, - message: nls.localize('read the release notes', "Welcome to {0} April Public Preview! Would you like to view the Getting Started Guide?", product.nameLong, pkg.version), - actions - }); - }); - } - - storageService.store(ProductContribution.KEY, pkg.version, StorageScope.GLOBAL); - } -} diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 3a1638cdf0..2dc56ac6f0 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -38,7 +38,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { OS } from 'vs/base/common/platform'; import { IRange } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { INotificationService, INotification, INotificationHandle, NoOpNotification } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, INotificationHandle, NoOpNotification, IPromptChoice } from 'vs/platform/notification/common/notification'; import { IConfirmation, IConfirmationResult, IConfirmationService } from 'vs/platform/dialogs/common/dialogs'; import { IPosition, Position as Pos } from 'vs/editor/common/core/position'; @@ -292,6 +292,10 @@ export class SimpleNotificationService implements INotificationService { return SimpleNotificationService.NO_OP; } + + public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + return SimpleNotificationService.NO_OP; + } } export class StandaloneCommandService implements ICommandService { diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index dd433da155..a3b1d542c2 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -19,7 +19,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { OS } from 'vs/base/common/platform'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { INotificationService, NoOpNotification, INotification } from 'vs/platform/notification/common/notification'; +import { INotificationService, NoOpNotification, INotification, IPromptChoice } from 'vs/platform/notification/common/notification'; function createContext(ctx: any) { return { @@ -138,6 +138,9 @@ suite('AbstractKeybindingService', () => { error: (message: any) => { showMessageCalls.push({ sev: Severity.Error, message }); return new NoOpNotification(); + }, + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): any { + throw new Error('not implemented'); } }; diff --git a/src/vs/platform/node/product.ts b/src/vs/platform/node/product.ts index 25af5ce560..f3e59444aa 100644 --- a/src/vs/platform/node/product.ts +++ b/src/vs/platform/node/product.ts @@ -62,6 +62,7 @@ export interface IProductConfiguration { reportIssueUrl: string; licenseUrl: string; privacyStatementUrl: string; + telemetryOptOutUrl: string; npsSurveyUrl: string; surveys: ISurveyData[]; checksums: { [path: string]: string; }; diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 4363668588..cf3a45e7d6 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -89,12 +89,12 @@ export interface INotificationProgress { done(): void; } -export interface INotificationHandle extends IDisposable { +export interface INotificationHandle { /** - * Will be fired once the notification is disposed. + * Will be fired once the notification is closed. */ - readonly onDidDispose: Event; + readonly onDidClose: Event; /** * Allows to indicate progress on the notification even after the @@ -118,6 +118,36 @@ export interface INotificationHandle extends IDisposable { * notification is already visible. */ updateActions(actions?: INotificationActions): void; + + /** + * Hide the notification and remove it from the notification center. + */ + close(): void; +} + +export interface IPromptChoice { + + /** + * Label to show for the choice to the user. + */ + label: string; + + /** + * Primary choices show up as buttons in the notification below the message. + * Secondary choices show up under the gear icon in the header of the notification. + */ + isSecondary?: boolean; + + /** + * Wether to keep the notification open after the choice was selected + * by the user. By default, will close the notification upon click. + */ + keepOpen?: boolean; + + /** + * Triggered when the user selects the choice. + */ + run: () => void; } export interface INotificationService { @@ -151,23 +181,34 @@ export interface INotificationService { * method if you need more control over the notification. */ error(message: NotificationMessage | NotificationMessage[]): void; + + /** + * Shows a prompt in the notification area with the provided choices. The prompt + * is non-modal. If you want to show a modal dialog instead, use `IDialogService`. + * + * @param onCancel will be called if the user closed the notification without picking + * any of the provided choices. + * + * @returns a handle on the notification to e.g. hide it or update message, buttons, etc. + */ + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle; } export class NoOpNotification implements INotificationHandle { readonly progress = new NoOpProgress(); - private _onDidDispose: Emitter = new Emitter(); + private readonly _onDidClose: Emitter = new Emitter(); - public get onDidDispose(): Event { - return this._onDidDispose.event; + public get onDidClose(): Event { + return this._onDidClose.event; } updateSeverity(severity: Severity): void { } updateMessage(message: NotificationMessage): void { } updateActions(actions?: INotificationActions): void { } - dispose(): void { - this._onDidDispose.dispose(); + close(): void { + this._onDidClose.dispose(); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts b/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts index 7add36a1cd..3706f5c50a 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts @@ -92,7 +92,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { // if promise has not been resolved yet, now is the time to ensure a return value // otherwise if already resolved it means the user clicked one of the buttons - once(messageHandle.onDidDispose)(() => { + once(messageHandle.onDidClose)(() => { resolve(undefined); }); }); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index d76110e707..ad35b912f0 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -282,7 +282,7 @@ export class NotificationsCenter extends Themable { // Dispose all while (this.model.notifications.length) { - this.model.notifications[0].dispose(); + this.model.notifications[0].close(); } } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index ea8797cb6c..d4b5dcbc52 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -115,7 +115,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl handler: (accessor, args?: any) => { const notification = getNotificationFromContext(accessor.get(IListService), args); if (notification) { - notification.dispose(); + notification.close(); } } }); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index fccffcf8e7..a48d66c1c4 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -164,7 +164,7 @@ export class NotificationsToasts extends Themable { })); // Remove when item gets disposed - once(item.onDidDispose)(() => { + once(item.onDidClose)(() => { this.removeToast(item); }); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index bc23c08e1f..87587fa091 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -440,7 +440,7 @@ export class NotificationTemplateRenderer { this.actionRunner.run(action, notification); // Hide notification - notification.dispose(); + notification.close(); })); this.inputDisposeables.push(attachButtonStyler(button, this.themeService)); diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index ed838794a1..9c1ac7426a 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -7,7 +7,7 @@ import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage } from 'vs/platform/notification/common/notification'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import Event, { Emitter, once } from 'vs/base/common/event'; +import Event, {Emitter, once } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { isPromiseCanceledError, isErrorWithActions } from 'vs/base/common/errors'; @@ -44,21 +44,21 @@ export interface INotificationChangeEvent { } export class NotificationHandle implements INotificationHandle { - private _onDidDispose: Emitter = new Emitter(); + private readonly _onDidClose: Emitter = new Emitter(); - constructor(private item: INotificationViewItem, private disposeItem: (item: INotificationViewItem) => void) { + constructor(private item: INotificationViewItem, private closeItem: (item: INotificationViewItem) => void) { this.registerListeners(); } private registerListeners(): void { - once(this.item.onDidDispose)(() => { - this._onDidDispose.fire(); - this._onDidDispose.dispose(); + once(this.item.onDidClose)(() => { + this._onDidClose.fire(); + this._onDidClose.dispose(); }); } - public get onDidDispose(): Event { - return this._onDidDispose.event; + public get onDidClose(): Event { + return this._onDidClose.event; } public get progress(): INotificationProgress { @@ -77,9 +77,9 @@ export class NotificationHandle implements INotificationHandle { this.item.updateActions(actions); } - public dispose(): void { - this.disposeItem(this.item); - this._onDidDispose.dispose(); + public close(): void { + this.closeItem(this.item); + this._onDidClose.dispose(); } } @@ -89,7 +89,7 @@ export class NotificationsModel implements INotificationsModel { private _notifications: INotificationViewItem[]; - private _onDidNotificationChange: Emitter; + private readonly _onDidNotificationChange: Emitter; private toDispose: IDisposable[]; constructor() { @@ -117,7 +117,7 @@ export class NotificationsModel implements INotificationsModel { // Deduplicate const duplicate = this.findNotification(item); if (duplicate) { - duplicate.dispose(); + duplicate.close(); } // Add to list as first entry @@ -127,15 +127,15 @@ export class NotificationsModel implements INotificationsModel { this._onDidNotificationChange.fire({ item, index: 0, kind: NotificationChangeType.ADD }); // Wrap into handle - return new NotificationHandle(item, item => this.disposeItem(item)); + return new NotificationHandle(item, item => this.closeItem(item)); } - private disposeItem(item: INotificationViewItem): void { + private closeItem(item: INotificationViewItem): void { const liveItem = this.findNotification(item); if (liveItem && liveItem !== item) { - liveItem.dispose(); // item could have been replaced with another one, make sure to dispose the live item + liveItem.close(); // item could have been replaced with another one, make sure to close the live item } else { - item.dispose(); // otherwise just dispose the item that was passed in + item.close(); // otherwise just close the item that was passed in } } @@ -174,7 +174,7 @@ export class NotificationsModel implements INotificationsModel { } }); - once(item.onDidDispose)(() => { + once(item.onDidClose)(() => { itemExpansionChangeListener.dispose(); itemLabelChangeListener.dispose(); @@ -204,7 +204,7 @@ export interface INotificationViewItem { readonly canCollapse: boolean; readonly onDidExpansionChange: Event; - readonly onDidDispose: Event; + readonly onDidClose: Event; readonly onDidLabelChange: Event; expand(): void; @@ -217,7 +217,7 @@ export interface INotificationViewItem { updateMessage(message: NotificationMessage): void; updateActions(actions?: INotificationActions): void; - dispose(): void; + close(): void; equals(item: INotificationViewItem); } @@ -253,7 +253,7 @@ export interface INotificationViewItemProgress extends INotificationProgress { export class NotificationViewItemProgress implements INotificationViewItemProgress { private _state: INotificationViewItemProgressState; - private _onDidChange: Emitter; + private readonly _onDidChange: Emitter; private toDispose: IDisposable[]; constructor() { @@ -358,9 +358,9 @@ export class NotificationViewItem implements INotificationViewItem { private _actions: INotificationActions; private _progress: NotificationViewItemProgress; - private _onDidExpansionChange: Emitter; - private _onDidDispose: Emitter; - private _onDidLabelChange: Emitter; + private readonly _onDidExpansionChange: Emitter; + private readonly _onDidClose: Emitter; + private readonly _onDidLabelChange: Emitter; public static create(notification: INotification): INotificationViewItem { if (!notification || !notification.message || isPromiseCanceledError(notification.message)) { @@ -435,8 +435,8 @@ export class NotificationViewItem implements INotificationViewItem { this._onDidLabelChange = new Emitter(); this.toDispose.push(this._onDidLabelChange); - this._onDidDispose = new Emitter(); - this.toDispose.push(this._onDidDispose); + this._onDidClose = new Emitter(); + this.toDispose.push(this._onDidClose); } private setActions(actions: INotificationActions): void { @@ -454,9 +454,6 @@ export class NotificationViewItem implements INotificationViewItem { this._actions = actions; this._expanded = actions.primary.length > 0; - - this.toDispose.push(...actions.primary); - this.toDispose.push(...actions.secondary); } public get onDidExpansionChange(): Event { @@ -467,8 +464,8 @@ export class NotificationViewItem implements INotificationViewItem { return this._onDidLabelChange.event; } - public get onDidDispose(): Event { - return this._onDidDispose.event; + public get onDidClose(): Event { + return this._onDidClose.event; } public get canCollapse(): boolean { @@ -559,13 +556,17 @@ export class NotificationViewItem implements INotificationViewItem { } } - public dispose(): void { - this._onDidDispose.fire(); + public close(): void { + this._onDidClose.fire(); this.toDispose = dispose(this.toDispose); } public equals(other: INotificationViewItem): boolean { + if (this.hasProgress() || other.hasProgress()) { + return false; + } + if (this._source !== other.source) { return false; } @@ -581,7 +582,7 @@ export class NotificationViewItem implements INotificationViewItem { } for (let i = 0; i < primaryActions.length; i++) { - if (primaryActions[i].id !== otherPrimaryActions[i].id) { + if ((primaryActions[i].id + primaryActions[i].label) !== (otherPrimaryActions[i].id + otherPrimaryActions[i].label)) { return false; } } diff --git a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts index ca8da3a0ee..1a70a93c60 100644 --- a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts @@ -101,7 +101,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi private onFileSavedOrReverted(resource: URI): void { const messageHandle = this.messages.get(resource); if (messageHandle) { - messageHandle.dispose(); + messageHandle.close(); this.messages.delete(resource); } } @@ -190,7 +190,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi const pendingResolveSaveConflictMessages: INotificationHandle[] = []; function clearPendingResolveSaveConflictMessages(): void { while (pendingResolveSaveConflictMessages.length > 0) { - pendingResolveSaveConflictMessages.pop().dispose(); + pendingResolveSaveConflictMessages.pop().close(); } } diff --git a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.contribution.ts b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.contribution.ts index 6c8ece2949..ab0d42f4e7 100644 --- a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.contribution.ts @@ -6,9 +6,15 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { GettingStarted } from './gettingStarted'; +import { TelemetryOptOut } from './telemetryOptOut'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +// {{SQL CARBON EDIT}} +// Registry +// .as(WorkbenchExtensions.Workbench) +// .registerWorkbenchContribution(GettingStarted, LifecyclePhase.Running); + Registry .as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(GettingStarted, LifecyclePhase.Running); \ No newline at end of file + .registerWorkbenchContribution(TelemetryOptOut, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts new file mode 100644 index 0000000000..095b240c59 --- /dev/null +++ b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import product from 'vs/platform/node/product'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import URI from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; + +export class TelemetryOptOut implements IWorkbenchContribution { + + private static TELEMETRY_OPT_OUT_SHOWN = 'workbench.telemetryOptOutShown'; + + constructor( + @IStorageService storageService: IStorageService, + @IOpenerService openerService: IOpenerService, + @INotificationService notificationService: INotificationService, + @IWindowService windowService: IWindowService, + @IWindowsService windowsService: IWindowsService, + @ITelemetryService telemetryService: ITelemetryService + ) { + if (!product.telemetryOptOutUrl || storageService.get(TelemetryOptOut.TELEMETRY_OPT_OUT_SHOWN)) { + return; + } + Promise.all([ + windowService.isFocused(), + windowsService.getWindowCount() + ]).then(([focused, count]) => { + if (!focused && count > 1) { + return null; + } + storageService.store(TelemetryOptOut.TELEMETRY_OPT_OUT_SHOWN, true); + + const optOutUrl = product.telemetryOptOutUrl; + const privacyUrl = product.privacyStatementUrl || product.telemetryOptOutUrl; + // {{SQL CARBON EDIT}} + const optOutNotice = localize('telemetryOptOut.optOutNotice', "Help improve SQL Operations Studio by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and how to [opt out]({1}).", privacyUrl, optOutUrl); + const optInNotice = localize('telemetryOptOut.optInNotice', "Help improve SQL Operations Studio by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and how to [opt in]({1}).", privacyUrl, optOutUrl); + + notificationService.prompt( + Severity.Info, + telemetryService.isOptedIn ? optOutNotice : optInNotice, + [{ + label: localize('telemetryOptOut.readMore', "Read More"), + run: () => openerService.open(URI.parse(optOutUrl)) + }] + ); + }) + .then(null, onUnexpectedError); + } +} diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogs.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogs.ts index 33b9b6fbd2..24c9d2b788 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogs.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogs.ts @@ -134,7 +134,7 @@ export class DialogService implements IChoiceService, IConfirmationService { c(index); if (closeNotification) { - handle.dispose(); + handle.close(); } return TPromise.as(void 0); @@ -171,9 +171,9 @@ export class DialogService implements IChoiceService, IConfirmationService { handle = this.notificationService.notify({ severity, message, actions }); // Cancel promise when notification gets disposed - once(handle.onDidDispose)(() => promise.cancel()); + once(handle.onDidClose)(() => promise.cancel()); - }, () => handle.dispose()); + }, () => handle.close()); return promise; } diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index 233ee1fd37..39f87cddb9 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -3,11 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice } from 'vs/platform/notification/common/notification'; import { INotificationsModel, NotificationsModel } from 'vs/workbench/common/notifications'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Action } from 'vs/base/common/actions'; +import { once } from 'vs/base/common/event'; export class NotificationService implements INotificationService { @@ -62,6 +63,51 @@ export class NotificationService implements INotificationService { return this.model.notify(notification); } + public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + let handle: INotificationHandle; + let choiceClicked = false; + + // Convert choices into primary/secondary actions + const actions: INotificationActions = { primary: [], secondary: [] }; + choices.forEach((choice, index) => { + const action = new Action(`workbench.dialog.choice.${index}`, choice.label, null, true, () => { + choiceClicked = true; + + // Pass to runner + choice.run(); + + // Close notification unless we are told to keep open + if (!choice.keepOpen) { + handle.close(); + } + + return TPromise.as(void 0); + }); + + if (!choice.isSecondary) { + actions.primary.push(action); + } else { + actions.secondary.push(action); + } + }); + + // Show notification with actions + handle = this.notify({ severity, message, actions }); + + once(handle.onDidClose)(() => { + + // Cleanup when notification gets disposed + dispose(...actions.primary, ...actions.secondary); + + // Indicate cancellation to the outside if no action was executed + if (!choiceClicked && typeof onCancel === 'function') { + onCancel(); + } + }); + + return handle; + } + public dispose(): void { this.toDispose = dispose(this.toDispose); } diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 86011304e9..f55b839d2a 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -96,11 +96,11 @@ suite('Notifications', () => { assert.equal(called, 1); called = 0; - item1.onDidDispose(() => { + item1.onDidClose(() => { called++; }); - item1.dispose(); + item1.close(); assert.equal(called, 1); // Error with Action @@ -157,11 +157,11 @@ suite('Notifications', () => { assert.equal(model.notifications.length, 3); let called = 0; - item1Handle.onDidDispose(() => { + item1Handle.onDidClose(() => { called++; }); - item1Handle.dispose(); + item1Handle.close(); assert.equal(called, 1); assert.equal(model.notifications.length, 2); assert.equal(lastEvent.item.severity, item1.severity); @@ -176,7 +176,7 @@ suite('Notifications', () => { assert.equal(lastEvent.index, 0); assert.equal(lastEvent.kind, NotificationChangeType.ADD); - item2Handle.dispose(); + item2Handle.close(); assert.equal(model.notifications.length, 1); assert.equal(lastEvent.item.severity, item2Duplicate.severity); assert.equal(lastEvent.item.message.value, item2Duplicate.message); diff --git a/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts b/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts index fbc418c2f9..e4c05f0fe6 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts @@ -9,7 +9,7 @@ import * as assert from 'assert'; import { MainThreadMessageService } from 'vs/workbench/api/electron-browser/mainThreadMessageService'; import { TPromise as Promise, TPromise } from 'vs/base/common/winjs.base'; import { IChoiceService } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService, INotification, NoOpNotification, INotificationHandle } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, NoOpNotification, INotificationHandle, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; const emptyChoiceService = new class implements IChoiceService { @@ -41,6 +41,9 @@ const emptyNotificationService = new class implements INotificationService { error(...args: any[]): never { throw new Error('not implemented'); } + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + throw new Error('not implemented'); + } }; class EmptyNotificationService implements INotificationService { @@ -64,6 +67,9 @@ class EmptyNotificationService implements INotificationService { error(message: any): void { throw new Error('Method not implemented.'); } + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + throw new Error('not implemented'); + } } suite('ExtHostMessageService', function () { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index ebcc8a0658..a6bda91e3e 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -63,7 +63,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { IChoiceService, IConfirmation, IConfirmationResult, IConfirmationService } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService, INotificationHandle, INotification, NoOpNotification } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotificationHandle, INotification, NoOpNotification, IPromptChoice } from 'vs/platform/notification/common/notification'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, void 0); @@ -331,6 +331,10 @@ export class TestNotificationService implements INotificationService { public notify(notification: INotification): INotificationHandle { return TestNotificationService.NO_OP; } + + public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + return TestNotificationService.NO_OP; + } } export class TestConfirmationService implements IConfirmationService { diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 946707d2d4..1ab643c2c4 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -112,8 +112,7 @@ import 'vs/workbench/parts/themes/electron-browser/themes.contribution'; import 'vs/workbench/parts/feedback/electron-browser/feedback.contribution'; -// {{SQL CARBON EDIT}} -// import 'vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.contribution'; +import 'vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.contribution'; import 'vs/workbench/parts/update/electron-browser/update.contribution';