Port VS Code telemetry opt-in dialog (#1130)

This commit is contained in:
Karl Burtram
2018-04-11 15:47:34 -07:00
committed by GitHub
parent ed10f984b6
commit cd0210c88a
22 changed files with 240 additions and 107 deletions

View File

@@ -16,6 +16,8 @@
"darwinBundleIdentifier": "com.sqlopsstudio.oss", "darwinBundleIdentifier": "com.sqlopsstudio.oss",
"reportIssueUrl": "https://github.com/Microsoft/sqlopsstudio/issues/new?labels=customer%20reported%20issue", "reportIssueUrl": "https://github.com/Microsoft/sqlopsstudio/issues/new?labels=customer%20reported%20issue",
"requestFeatureUrl": "https://github.com/Microsoft/sqlopsstudio/issues/new?labels=feature-request", "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", "urlProtocol": "sqlops",
"enableTelemetry": true, "enableTelemetry": true,
"aiConfig": { "aiConfig": {

View File

@@ -12,7 +12,7 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; 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'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
new Actions.BackupAction().registerTask(); new Actions.BackupAction().registerTask();
@@ -21,8 +21,5 @@ new Actions.NewQueryAction().registerTask();
new Actions.ConfigureDashboardAction().registerTask(); new Actions.ConfigureDashboardAction().registerTask();
// add product update and release notes contributions // add product update and release notes contributions
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(ProductContribution, LifecyclePhase.Running);
Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions) Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions)
.registerWorkbenchAction(new SyncActionDescriptor(ShowCurrentReleaseNotesAction, ShowCurrentReleaseNotesAction.ID, ShowCurrentReleaseNotesAction.LABEL), 'Show Getting Started'); .registerWorkbenchAction(new SyncActionDescriptor(ShowCurrentReleaseNotesAction, ShowCurrentReleaseNotesAction.ID, ShowCurrentReleaseNotesAction.LABEL), 'Show Getting Started');

View File

@@ -49,39 +49,3 @@ export class ShowCurrentReleaseNotesAction extends AbstractShowReleaseNotesActio
super(id, label, pkg.version, editorService, instantiationService); 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);
}
}

View File

@@ -38,7 +38,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe
import { OS } from 'vs/base/common/platform'; import { OS } from 'vs/base/common/platform';
import { IRange } from 'vs/editor/common/core/range'; import { IRange } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model'; 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 { IConfirmation, IConfirmationResult, IConfirmationService } from 'vs/platform/dialogs/common/dialogs';
import { IPosition, Position as Pos } from 'vs/editor/common/core/position'; import { IPosition, Position as Pos } from 'vs/editor/common/core/position';
@@ -292,6 +292,10 @@ export class SimpleNotificationService implements INotificationService {
return SimpleNotificationService.NO_OP; return SimpleNotificationService.NO_OP;
} }
public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle {
return SimpleNotificationService.NO_OP;
}
} }
export class StandaloneCommandService implements ICommandService { export class StandaloneCommandService implements ICommandService {

View File

@@ -19,7 +19,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe
import { OS } from 'vs/base/common/platform'; import { OS } from 'vs/base/common/platform';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; 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) { function createContext(ctx: any) {
return { return {
@@ -138,6 +138,9 @@ suite('AbstractKeybindingService', () => {
error: (message: any) => { error: (message: any) => {
showMessageCalls.push({ sev: Severity.Error, message }); showMessageCalls.push({ sev: Severity.Error, message });
return new NoOpNotification(); return new NoOpNotification();
},
prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): any {
throw new Error('not implemented');
} }
}; };

View File

@@ -62,6 +62,7 @@ export interface IProductConfiguration {
reportIssueUrl: string; reportIssueUrl: string;
licenseUrl: string; licenseUrl: string;
privacyStatementUrl: string; privacyStatementUrl: string;
telemetryOptOutUrl: string;
npsSurveyUrl: string; npsSurveyUrl: string;
surveys: ISurveyData[]; surveys: ISurveyData[];
checksums: { [path: string]: string; }; checksums: { [path: string]: string; };

View File

@@ -89,12 +89,12 @@ export interface INotificationProgress {
done(): void; 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<void>; readonly onDidClose: Event<void>;
/** /**
* Allows to indicate progress on the notification even after the * Allows to indicate progress on the notification even after the
@@ -118,6 +118,36 @@ export interface INotificationHandle extends IDisposable {
* notification is already visible. * notification is already visible.
*/ */
updateActions(actions?: INotificationActions): void; 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 { export interface INotificationService {
@@ -151,23 +181,34 @@ export interface INotificationService {
* method if you need more control over the notification. * method if you need more control over the notification.
*/ */
error(message: NotificationMessage | NotificationMessage[]): void; 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 { export class NoOpNotification implements INotificationHandle {
readonly progress = new NoOpProgress(); readonly progress = new NoOpProgress();
private _onDidDispose: Emitter<void> = new Emitter(); private readonly _onDidClose: Emitter<void> = new Emitter();
public get onDidDispose(): Event<void> { public get onDidClose(): Event<void> {
return this._onDidDispose.event; return this._onDidClose.event;
} }
updateSeverity(severity: Severity): void { } updateSeverity(severity: Severity): void { }
updateMessage(message: NotificationMessage): void { } updateMessage(message: NotificationMessage): void { }
updateActions(actions?: INotificationActions): void { } updateActions(actions?: INotificationActions): void { }
dispose(): void { close(): void {
this._onDidDispose.dispose(); this._onDidClose.dispose();
} }
} }

View File

@@ -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 // 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 // otherwise if already resolved it means the user clicked one of the buttons
once(messageHandle.onDidDispose)(() => { once(messageHandle.onDidClose)(() => {
resolve(undefined); resolve(undefined);
}); });
}); });

View File

@@ -282,7 +282,7 @@ export class NotificationsCenter extends Themable {
// Dispose all // Dispose all
while (this.model.notifications.length) { while (this.model.notifications.length) {
this.model.notifications[0].dispose(); this.model.notifications[0].close();
} }
} }
} }

View File

@@ -115,7 +115,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl
handler: (accessor, args?: any) => { handler: (accessor, args?: any) => {
const notification = getNotificationFromContext(accessor.get(IListService), args); const notification = getNotificationFromContext(accessor.get(IListService), args);
if (notification) { if (notification) {
notification.dispose(); notification.close();
} }
} }
}); });

View File

@@ -164,7 +164,7 @@ export class NotificationsToasts extends Themable {
})); }));
// Remove when item gets disposed // Remove when item gets disposed
once(item.onDidDispose)(() => { once(item.onDidClose)(() => {
this.removeToast(item); this.removeToast(item);
}); });

View File

@@ -440,7 +440,7 @@ export class NotificationTemplateRenderer {
this.actionRunner.run(action, notification); this.actionRunner.run(action, notification);
// Hide notification // Hide notification
notification.dispose(); notification.close();
})); }));
this.inputDisposeables.push(attachButtonStyler(button, this.themeService)); this.inputDisposeables.push(attachButtonStyler(button, this.themeService));

View File

@@ -44,21 +44,21 @@ export interface INotificationChangeEvent {
} }
export class NotificationHandle implements INotificationHandle { export class NotificationHandle implements INotificationHandle {
private _onDidDispose: Emitter<void> = new Emitter(); private readonly _onDidClose: Emitter<void> = new Emitter();
constructor(private item: INotificationViewItem, private disposeItem: (item: INotificationViewItem) => void) { constructor(private item: INotificationViewItem, private closeItem: (item: INotificationViewItem) => void) {
this.registerListeners(); this.registerListeners();
} }
private registerListeners(): void { private registerListeners(): void {
once(this.item.onDidDispose)(() => { once(this.item.onDidClose)(() => {
this._onDidDispose.fire(); this._onDidClose.fire();
this._onDidDispose.dispose(); this._onDidClose.dispose();
}); });
} }
public get onDidDispose(): Event<void> { public get onDidClose(): Event<void> {
return this._onDidDispose.event; return this._onDidClose.event;
} }
public get progress(): INotificationProgress { public get progress(): INotificationProgress {
@@ -77,9 +77,9 @@ export class NotificationHandle implements INotificationHandle {
this.item.updateActions(actions); this.item.updateActions(actions);
} }
public dispose(): void { public close(): void {
this.disposeItem(this.item); this.closeItem(this.item);
this._onDidDispose.dispose(); this._onDidClose.dispose();
} }
} }
@@ -89,7 +89,7 @@ export class NotificationsModel implements INotificationsModel {
private _notifications: INotificationViewItem[]; private _notifications: INotificationViewItem[];
private _onDidNotificationChange: Emitter<INotificationChangeEvent>; private readonly _onDidNotificationChange: Emitter<INotificationChangeEvent>;
private toDispose: IDisposable[]; private toDispose: IDisposable[];
constructor() { constructor() {
@@ -117,7 +117,7 @@ export class NotificationsModel implements INotificationsModel {
// Deduplicate // Deduplicate
const duplicate = this.findNotification(item); const duplicate = this.findNotification(item);
if (duplicate) { if (duplicate) {
duplicate.dispose(); duplicate.close();
} }
// Add to list as first entry // Add to list as first entry
@@ -127,15 +127,15 @@ export class NotificationsModel implements INotificationsModel {
this._onDidNotificationChange.fire({ item, index: 0, kind: NotificationChangeType.ADD }); this._onDidNotificationChange.fire({ item, index: 0, kind: NotificationChangeType.ADD });
// Wrap into handle // 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); const liveItem = this.findNotification(item);
if (liveItem && liveItem !== 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 { } 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(); itemExpansionChangeListener.dispose();
itemLabelChangeListener.dispose(); itemLabelChangeListener.dispose();
@@ -204,7 +204,7 @@ export interface INotificationViewItem {
readonly canCollapse: boolean; readonly canCollapse: boolean;
readonly onDidExpansionChange: Event<void>; readonly onDidExpansionChange: Event<void>;
readonly onDidDispose: Event<void>; readonly onDidClose: Event<void>;
readonly onDidLabelChange: Event<INotificationViewItemLabelChangeEvent>; readonly onDidLabelChange: Event<INotificationViewItemLabelChangeEvent>;
expand(): void; expand(): void;
@@ -217,7 +217,7 @@ export interface INotificationViewItem {
updateMessage(message: NotificationMessage): void; updateMessage(message: NotificationMessage): void;
updateActions(actions?: INotificationActions): void; updateActions(actions?: INotificationActions): void;
dispose(): void; close(): void;
equals(item: INotificationViewItem); equals(item: INotificationViewItem);
} }
@@ -253,7 +253,7 @@ export interface INotificationViewItemProgress extends INotificationProgress {
export class NotificationViewItemProgress implements INotificationViewItemProgress { export class NotificationViewItemProgress implements INotificationViewItemProgress {
private _state: INotificationViewItemProgressState; private _state: INotificationViewItemProgressState;
private _onDidChange: Emitter<void>; private readonly _onDidChange: Emitter<void>;
private toDispose: IDisposable[]; private toDispose: IDisposable[];
constructor() { constructor() {
@@ -358,9 +358,9 @@ export class NotificationViewItem implements INotificationViewItem {
private _actions: INotificationActions; private _actions: INotificationActions;
private _progress: NotificationViewItemProgress; private _progress: NotificationViewItemProgress;
private _onDidExpansionChange: Emitter<void>; private readonly _onDidExpansionChange: Emitter<void>;
private _onDidDispose: Emitter<void>; private readonly _onDidClose: Emitter<void>;
private _onDidLabelChange: Emitter<INotificationViewItemLabelChangeEvent>; private readonly _onDidLabelChange: Emitter<INotificationViewItemLabelChangeEvent>;
public static create(notification: INotification): INotificationViewItem { public static create(notification: INotification): INotificationViewItem {
if (!notification || !notification.message || isPromiseCanceledError(notification.message)) { if (!notification || !notification.message || isPromiseCanceledError(notification.message)) {
@@ -435,8 +435,8 @@ export class NotificationViewItem implements INotificationViewItem {
this._onDidLabelChange = new Emitter<INotificationViewItemLabelChangeEvent>(); this._onDidLabelChange = new Emitter<INotificationViewItemLabelChangeEvent>();
this.toDispose.push(this._onDidLabelChange); this.toDispose.push(this._onDidLabelChange);
this._onDidDispose = new Emitter<void>(); this._onDidClose = new Emitter<void>();
this.toDispose.push(this._onDidDispose); this.toDispose.push(this._onDidClose);
} }
private setActions(actions: INotificationActions): void { private setActions(actions: INotificationActions): void {
@@ -454,9 +454,6 @@ export class NotificationViewItem implements INotificationViewItem {
this._actions = actions; this._actions = actions;
this._expanded = actions.primary.length > 0; this._expanded = actions.primary.length > 0;
this.toDispose.push(...actions.primary);
this.toDispose.push(...actions.secondary);
} }
public get onDidExpansionChange(): Event<void> { public get onDidExpansionChange(): Event<void> {
@@ -467,8 +464,8 @@ export class NotificationViewItem implements INotificationViewItem {
return this._onDidLabelChange.event; return this._onDidLabelChange.event;
} }
public get onDidDispose(): Event<void> { public get onDidClose(): Event<void> {
return this._onDidDispose.event; return this._onDidClose.event;
} }
public get canCollapse(): boolean { public get canCollapse(): boolean {
@@ -559,13 +556,17 @@ export class NotificationViewItem implements INotificationViewItem {
} }
} }
public dispose(): void { public close(): void {
this._onDidDispose.fire(); this._onDidClose.fire();
this.toDispose = dispose(this.toDispose); this.toDispose = dispose(this.toDispose);
} }
public equals(other: INotificationViewItem): boolean { public equals(other: INotificationViewItem): boolean {
if (this.hasProgress() || other.hasProgress()) {
return false;
}
if (this._source !== other.source) { if (this._source !== other.source) {
return false; return false;
} }
@@ -581,7 +582,7 @@ export class NotificationViewItem implements INotificationViewItem {
} }
for (let i = 0; i < primaryActions.length; i++) { 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; return false;
} }
} }

View File

@@ -101,7 +101,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi
private onFileSavedOrReverted(resource: URI): void { private onFileSavedOrReverted(resource: URI): void {
const messageHandle = this.messages.get(resource); const messageHandle = this.messages.get(resource);
if (messageHandle) { if (messageHandle) {
messageHandle.dispose(); messageHandle.close();
this.messages.delete(resource); this.messages.delete(resource);
} }
} }
@@ -190,7 +190,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi
const pendingResolveSaveConflictMessages: INotificationHandle[] = []; const pendingResolveSaveConflictMessages: INotificationHandle[] = [];
function clearPendingResolveSaveConflictMessages(): void { function clearPendingResolveSaveConflictMessages(): void {
while (pendingResolveSaveConflictMessages.length > 0) { while (pendingResolveSaveConflictMessages.length > 0) {
pendingResolveSaveConflictMessages.pop().dispose(); pendingResolveSaveConflictMessages.pop().close();
} }
} }

View File

@@ -6,9 +6,15 @@
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { GettingStarted } from './gettingStarted'; import { GettingStarted } from './gettingStarted';
import { TelemetryOptOut } from './telemetryOptOut';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
// {{SQL CARBON EDIT}}
// Registry
// .as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
// .registerWorkbenchContribution(GettingStarted, LifecyclePhase.Running);
Registry Registry
.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) .as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(GettingStarted, LifecyclePhase.Running); .registerWorkbenchContribution(TelemetryOptOut, LifecyclePhase.Eventually);

View File

@@ -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);
}
}

View File

@@ -134,7 +134,7 @@ export class DialogService implements IChoiceService, IConfirmationService {
c(index); c(index);
if (closeNotification) { if (closeNotification) {
handle.dispose(); handle.close();
} }
return TPromise.as(void 0); return TPromise.as(void 0);
@@ -171,9 +171,9 @@ export class DialogService implements IChoiceService, IConfirmationService {
handle = this.notificationService.notify({ severity, message, actions }); handle = this.notificationService.notify({ severity, message, actions });
// Cancel promise when notification gets disposed // Cancel promise when notification gets disposed
once(handle.onDidDispose)(() => promise.cancel()); once(handle.onDidClose)(() => promise.cancel());
}, () => handle.dispose()); }, () => handle.close());
return promise; return promise;
} }

View File

@@ -3,11 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
'use strict'; import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice } from 'vs/platform/notification/common/notification';
import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage } from 'vs/platform/notification/common/notification';
import { INotificationsModel, NotificationsModel } from 'vs/workbench/common/notifications'; import { INotificationsModel, NotificationsModel } from 'vs/workbench/common/notifications';
import { IDisposable, dispose } from 'vs/base/common/lifecycle'; 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 { export class NotificationService implements INotificationService {
@@ -62,6 +63,51 @@ export class NotificationService implements INotificationService {
return this.model.notify(notification); 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 { public dispose(): void {
this.toDispose = dispose(this.toDispose); this.toDispose = dispose(this.toDispose);
} }

View File

@@ -96,11 +96,11 @@ suite('Notifications', () => {
assert.equal(called, 1); assert.equal(called, 1);
called = 0; called = 0;
item1.onDidDispose(() => { item1.onDidClose(() => {
called++; called++;
}); });
item1.dispose(); item1.close();
assert.equal(called, 1); assert.equal(called, 1);
// Error with Action // Error with Action
@@ -157,11 +157,11 @@ suite('Notifications', () => {
assert.equal(model.notifications.length, 3); assert.equal(model.notifications.length, 3);
let called = 0; let called = 0;
item1Handle.onDidDispose(() => { item1Handle.onDidClose(() => {
called++; called++;
}); });
item1Handle.dispose(); item1Handle.close();
assert.equal(called, 1); assert.equal(called, 1);
assert.equal(model.notifications.length, 2); assert.equal(model.notifications.length, 2);
assert.equal(lastEvent.item.severity, item1.severity); assert.equal(lastEvent.item.severity, item1.severity);
@@ -176,7 +176,7 @@ suite('Notifications', () => {
assert.equal(lastEvent.index, 0); assert.equal(lastEvent.index, 0);
assert.equal(lastEvent.kind, NotificationChangeType.ADD); assert.equal(lastEvent.kind, NotificationChangeType.ADD);
item2Handle.dispose(); item2Handle.close();
assert.equal(model.notifications.length, 1); assert.equal(model.notifications.length, 1);
assert.equal(lastEvent.item.severity, item2Duplicate.severity); assert.equal(lastEvent.item.severity, item2Duplicate.severity);
assert.equal(lastEvent.item.message.value, item2Duplicate.message); assert.equal(lastEvent.item.message.value, item2Duplicate.message);

View File

@@ -9,7 +9,7 @@ import * as assert from 'assert';
import { MainThreadMessageService } from 'vs/workbench/api/electron-browser/mainThreadMessageService'; import { MainThreadMessageService } from 'vs/workbench/api/electron-browser/mainThreadMessageService';
import { TPromise as Promise, TPromise } from 'vs/base/common/winjs.base'; import { TPromise as Promise, TPromise } from 'vs/base/common/winjs.base';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs'; 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'; import { ICommandService } from 'vs/platform/commands/common/commands';
const emptyChoiceService = new class implements IChoiceService { const emptyChoiceService = new class implements IChoiceService {
@@ -41,6 +41,9 @@ const emptyNotificationService = new class implements INotificationService {
error(...args: any[]): never { error(...args: any[]): never {
throw new Error('not implemented'); throw new Error('not implemented');
} }
prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle {
throw new Error('not implemented');
}
}; };
class EmptyNotificationService implements INotificationService { class EmptyNotificationService implements INotificationService {
@@ -64,6 +67,9 @@ class EmptyNotificationService implements INotificationService {
error(message: any): void { error(message: any): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle {
throw new Error('not implemented');
}
} }
suite('ExtHostMessageService', function () { suite('ExtHostMessageService', function () {

View File

@@ -63,7 +63,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe
import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference } from 'vs/editor/common/model'; import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference } from 'vs/editor/common/model';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { IChoiceService, IConfirmation, IConfirmationResult, IConfirmationService } from 'vs/platform/dialogs/common/dialogs'; 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 { export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput {
return instantiationService.createInstance(FileEditorInput, resource, void 0); return instantiationService.createInstance(FileEditorInput, resource, void 0);
@@ -331,6 +331,10 @@ export class TestNotificationService implements INotificationService {
public notify(notification: INotification): INotificationHandle { public notify(notification: INotification): INotificationHandle {
return TestNotificationService.NO_OP; return TestNotificationService.NO_OP;
} }
public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle {
return TestNotificationService.NO_OP;
}
} }
export class TestConfirmationService implements IConfirmationService { export class TestConfirmationService implements IConfirmationService {

View File

@@ -112,8 +112,7 @@ import 'vs/workbench/parts/themes/electron-browser/themes.contribution';
import 'vs/workbench/parts/feedback/electron-browser/feedback.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'; import 'vs/workbench/parts/update/electron-browser/update.contribution';