allow for markdown dialogs and custom icons

This commit is contained in:
SteVen Batten
2021-04-15 14:03:38 -07:00
parent d531f3b054
commit 6e84c224f4
12 changed files with 178 additions and 47 deletions
+33 -18
View File
@@ -34,6 +34,8 @@ export interface IDialogOptions {
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
readonly inputs?: IDialogInputOptions[];
readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void;
readonly renderBody?: (container: HTMLElement) => void;
readonly icon?: Codicon;
}
export interface IDialogResult {
@@ -97,14 +99,23 @@ export class Dialog extends Disposable {
this.iconElement = messageRowElement.appendChild($('.dialog-icon'));
const messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
if (this.options.detail) {
if (this.options.detail || this.options.renderBody) {
const messageElement = messageContainer.appendChild($('.dialog-message'));
const messageTextElement = messageElement.appendChild($('.dialog-message-text'));
messageTextElement.innerText = this.message;
}
this.messageDetailElement = messageContainer.appendChild($('.dialog-message-detail'));
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
if (this.options.detail || !this.options.renderBody) {
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
} else {
this.messageDetailElement.style.display = 'none';
}
if (this.options.renderBody) {
const customBody = messageContainer.appendChild($('.dialog-message-body'));
this.options.renderBody(customBody);
}
if (this.options.inputs) {
this.inputs = this.options.inputs.map(input => {
@@ -313,22 +324,26 @@ export class Dialog extends Disposable {
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray, spinModifierClassName);
switch (this.options.type) {
case 'error':
this.iconElement.classList.add(...dialogErrorIcon.classNamesArray);
break;
case 'warning':
this.iconElement.classList.add(...dialogWarningIcon.classNamesArray);
break;
case 'pending':
this.iconElement.classList.add(...Codicon.loading.classNamesArray, spinModifierClassName);
break;
case 'none':
case 'info':
case 'question':
default:
this.iconElement.classList.add(...dialogInfoIcon.classNamesArray);
break;
if (this.options.icon) {
this.iconElement.classList.add(...this.options.icon.classNamesArray);
} else {
switch (this.options.type) {
case 'error':
this.iconElement.classList.add(...dialogErrorIcon.classNamesArray);
break;
case 'warning':
this.iconElement.classList.add(...dialogWarningIcon.classNamesArray);
break;
case 'pending':
this.iconElement.classList.add(...Codicon.loading.classNamesArray, spinModifierClassName);
break;
case 'none':
case 'info':
case 'question':
default:
this.iconElement.classList.add(...dialogInfoIcon.classNamesArray);
break;
}
}
const actionBar = this._register(new ActionBar(this.toolbarContainer, {}));
+14 -1
View File
@@ -9,6 +9,8 @@ import { URI } from 'vs/base/common/uri';
import { basename } from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { Codicon } from 'vs/base/common/codicons';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export interface FileFilter {
extensions: string[];
@@ -178,11 +180,22 @@ export interface IOpenDialogOptions {
export const IDialogService = createDecorator<IDialogService>('dialogService');
export interface ICustomDialogOptions {
markdownDetails?: ICustomDialogMarkdown[]
classes?: string[];
icon?: Codicon;
}
export interface ICustomDialogMarkdown {
markdown: IMarkdownString,
classes?: string[]
}
export interface IDialogOptions {
cancelId?: number;
detail?: string;
checkbox?: ICheckbox;
useCustom?: boolean;
custom?: boolean | ICustomDialogOptions;
}
export interface IInput {
@@ -121,7 +121,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape {
cancelId = buttons.length - 1;
}
const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId, useCustom });
const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId, custom: useCustom });
return choice === commands.length ? undefined : commands[choice].handle;
}
}
@@ -17,6 +17,7 @@ import { BrowserDialogHandler } from 'vs/workbench/browser/parts/dialogs/dialogH
import { DialogService } from 'vs/workbench/services/dialogs/common/dialogService';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class DialogHandlerContribution extends Disposable implements IWorkbenchContribution {
private readonly model: IDialogsModel;
@@ -30,12 +31,13 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC
@ILayoutService layoutService: ILayoutService,
@IThemeService themeService: IThemeService,
@IKeybindingService keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IProductService productService: IProductService,
@IClipboardService clipboardService: IClipboardService
) {
super();
this.impl = new BrowserDialogHandler(logService, layoutService, themeService, keybindingService, productService, clipboardService);
this.impl = new BrowserDialogHandler(logService, layoutService, themeService, keybindingService, instantiationService, productService, clipboardService);
this.model = (this.dialogService as DialogService).model;
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IDialogOptions, IConfirmation, IConfirmationResult, DialogType, IShowResult, IInputResult, ICheckbox, IInput, IDialogHandler } from 'vs/platform/dialogs/common/dialogs';
import { IDialogOptions, IConfirmation, IConfirmationResult, DialogType, IShowResult, IInputResult, ICheckbox, IInput, IDialogHandler, ICustomDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ILogService } from 'vs/platform/log/common/log';
import Severity from 'vs/base/common/severity';
@@ -18,6 +18,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IProductService } from 'vs/platform/product/common/productService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { fromNow } from 'vs/base/common/date';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
export class BrowserDialogHandler implements IDialogHandler {
@@ -30,14 +32,19 @@ export class BrowserDialogHandler implements IDialogHandler {
'editor.action.clipboardPasteAction'
];
private readonly markdownRenderer: MarkdownRenderer;
constructor(
@ILogService private readonly logService: ILogService,
@ILayoutService private readonly layoutService: ILayoutService,
@IThemeService private readonly themeService: IThemeService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IProductService private readonly productService: IProductService,
@IClipboardService private readonly clipboardService: IClipboardService
) { }
) {
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
}
async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
this.logService.trace('DialogService#confirm', confirmation.message);
@@ -67,7 +74,7 @@ export class BrowserDialogHandler implements IDialogHandler {
async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult> {
this.logService.trace('DialogService#show', message);
const result = await this.doShow(this.getDialogType(severity), message, buttons, options?.detail, options?.cancelId, options?.checkbox);
const result = await this.doShow(this.getDialogType(severity), message, buttons, options?.detail, options?.cancelId, options?.checkbox, undefined, typeof options?.custom === 'object' ? options.custom : undefined);
return {
choice: result.button,
@@ -75,8 +82,19 @@ export class BrowserDialogHandler implements IDialogHandler {
};
}
private async doShow(type: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending' | undefined, message: string, buttons: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInput[]): Promise<IDialogResult> {
private async doShow(type: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending' | undefined, message: string, buttons: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInput[], customOptions?: ICustomDialogOptions): Promise<IDialogResult> {
const dialogDisposables = new DisposableStore();
const renderBody = customOptions ? (parent: HTMLElement) => {
parent.classList.add(...(customOptions.classes || []));
(customOptions.markdownDetails || []).forEach(markdownDetail => {
const result = this.markdownRenderer.render(markdownDetail.markdown);
parent.appendChild(result.element);
result.element.classList.add(...(markdownDetail.classes || []));
dialogDisposables.add(result);
});
} : undefined;
const dialog = new Dialog(
this.layoutService.container,
message,
@@ -93,6 +111,8 @@ export class BrowserDialogHandler implements IDialogHandler {
}
}
},
renderBody,
icon: customOptions?.icon,
checkboxLabel: checkbox?.label,
checkboxChecked: checkbox?.checked,
inputs
@@ -921,7 +921,7 @@ class RemoteAgentConnectionStatusListener extends Disposable implements IWorkben
console.log(`Error handled: Not showing a notification for the error.`);
} else if (!this._reloadWindowShown) {
this._reloadWindowShown = true;
dialogService.show(Severity.Error, nls.localize('reconnectionPermanentFailure', "Cannot reconnect. Please reload the window."), [nls.localize('reloadWindow', "Reload Window"), nls.localize('cancel', "Cancel")], { cancelId: 1, useCustom: true }).then(result => {
dialogService.show(Severity.Error, nls.localize('reconnectionPermanentFailure', "Cannot reconnect. Please reload the window."), [nls.localize('reloadWindow', "Reload Window"), nls.localize('cancel', "Cancel")], { cancelId: 1, custom: true }).then(result => {
// Reload the window
if (result.choice === 0) {
commandService.executeCommand(ReloadWindowAction.ID);
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -36,7 +36,10 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
import { isWeb } from 'vs/base/common/platform';
import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys';
import { dirname, resolve } from 'vs/base/common/path';
import product from 'vs/platform/product/common/product';
import { FileAccess } from 'vs/base/common/network';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { MarkdownString } from 'vs/base/common/htmlContent';
const workspaceTrustIcon = registerIcon('workspace-trust-icon', Codicon.shield, localize('workspaceTrustIcon', "Icon for workspace trust badge."));
@@ -45,7 +48,7 @@ const workspaceTrustIcon = registerIcon('workspace-trust-icon', Codicon.shield,
*/
export class WorkspaceTrustRequestHandler extends Disposable implements IWorkbenchContribution {
private readonly badgeDisposable = this._register(new MutableDisposable());
private shouldShowManagementEditor = true;
private shouldShowIntroduction = true;
constructor(
@IDialogService private readonly dialogService: IDialogService,
@@ -71,11 +74,63 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben
priority: 10
});
const managementEditorShownKey = 'workspace.trust.management.shown';
const seen = this.storageService.getBoolean(managementEditorShownKey, StorageScope.WORKSPACE, false);
if (!seen && this.shouldShowManagementEditor) {
this.storageService.store(managementEditorShownKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
this.commandService.executeCommand('workbench.trust.manage');
const workspaceTrustIntroDialogDoNotShowAgainKey = 'workspace.trust.introduction.doNotShowAgain';
const doNotShowAgain = this.storageService.getBoolean(workspaceTrustIntroDialogDoNotShowAgainKey, StorageScope.GLOBAL, false);
if (!doNotShowAgain && this.shouldShowIntroduction) {
// Show welcome dialog
(async () => {
const result = await this.dialogService.show(
Severity.Info,
localize('workspaceTrust', "Introducing Workspace Trust"),
[
localize('manageTrust', "Manage"),
localize('close', "Close")
],
{
custom: {
icon: Codicon.shield,
classes: ['workspace-trust-intro-dialog'],
markdownDetails: [
{
markdown: new MarkdownString(localize('workspaceTrustDescription', "{0} provides many powerful features that rely on the files that are open in the current workspace. This can mean unintended code execution from the workspace and should only happen if you trust the source of the files you have open.", 'VS Code' || product.nameShort)),
},
{
markdown: new MarkdownString(`![${localize('altTextTrustedBadge', "Shield Badge on Activity Bar")}](${FileAccess.asBrowserUri('vs/workbench/contrib/workspace/browser/media/trusted-badge.png', require).toString(true)})\n*${localize('workspaceTrustBadgeDescription', "When features are disabled in an untrusted workspace, you will see this shield icon in the Activity Bar.")}*`),
classes: ['workspace-trust-dialog-image-row', 'badge-row']
},
{
markdown: new MarkdownString(`![${localize('altTextUntrustedStatus', "Workspace Trust Status Bar Entry")}](${FileAccess.asBrowserUri('vs/workbench/contrib/workspace/browser/media/untrusted-status.png', require).toString(true)})\n*${localize('workspaceTrustUntrustedDescription', "When the workspace is untrusted, you will see this status bar entry. It is hidden when the workspace is trusted.")}*`),
classes: ['workspace-trust-dialog-image-row', 'status-bar']
},
{
markdown: new MarkdownString(localize('seeTheDocs', "Manage your Workspace Trust configuration now to learn more or [see our docs for additional information](https://aka.ms/vscode-workspace-trust).")),
}
],
},
cancelId: 1, // Close
checkbox: {
label: localize('dontShowAgain', "Don't show this message again"),
checked: false,
}
}
);
// Dialog result
switch (result.choice) {
case 0:
this.workspaceTrustRequestService.cancelRequest();
await this.commandService.executeCommand('workbench.trust.manage');
break;
case 1:
this.workspaceTrustRequestService.completeRequest(undefined);
break;
}
if (result.checkboxChecked) {
this.storageService.store(workspaceTrustIntroDialogDoNotShowAgainKey, true, StorageScope.GLOBAL, StorageTarget.USER);
}
})();
}
}
}
@@ -101,13 +156,13 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben
// Dialog
const result = await this.dialogService.show(
Severity.Warning,
Severity.Info,
localize('immediateTrustRequestTitle', "Do you trust the files in this folder?"),
buttons.map(b => b.label),
{
cancelId: buttons.findIndex(b => b.type === 'Cancel'),
detail: localize('immediateTrustRequestDetail', "{0}\n\nYou should only trust this workspace if you trust its source. Using an untrusted workspace may compromise your device or personal information.", message),
useCustom: true
custom: { icon: Codicon.shield }
}
);
@@ -151,15 +206,19 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben
if (trusted && (e.changes.added.length || e.changes.changed.length)) {
const addedFoldersTrustStateInfo = e.changes.added.map(folder => this.workspaceTrustStorageService.getFolderTrustStateInfo(folder.uri));
if (!addedFoldersTrustStateInfo.map(i => i.trusted).every(trusted => trusted)) {
const result = await this.dialogService.confirm({
message: localize('addWorkspaceFolderMessage', "Do you trust the files in this folder?"),
detail: localize('addWorkspaceFolderDetail', "You are adding files to a trusted workspace that are not currently trusted. Do you want to trust the new files?"),
primaryButton: localize('yes', 'Yes'),
secondaryButton: localize('no', 'No')
});
const result = await this.dialogService.show(
Severity.Info,
localize('addWorkspaceFolderMessage', "Do you trust the files in this folder?"),
[localize('yes', 'Yes'), localize('no', 'No')],
{
detail: localize('addWorkspaceFolderDetail', "You are adding files to a trusted workspace that are not currently trusted. Do you want to trust the new files?"),
cancelId: 1,
custom: { icon: Codicon.shield }
}
);
// Mark added/changed folders as trusted
this.workspaceTrustStorageService.setFoldersTrust(addedFoldersTrustStateInfo.map(i => i.uri), result.confirmed);
this.workspaceTrustStorageService.setFoldersTrust(addedFoldersTrustStateInfo.map(i => i.uri), result.choice === 0);
resolve();
}
@@ -170,7 +229,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben
}));
// Don't auto-show the UX editor if the request is 5 seconds after startup
setTimeout(() => { this.shouldShowManagementEditor = false; }, 5000);
setTimeout(() => { this.shouldShowIntroduction = false; }, 5000);
}
}
@@ -237,3 +237,23 @@
margin-left: 4px;
min-width: 20%;
}
.workspace-trust-intro-dialog {
min-width: min(50vw, 500px);
padding-right: 24px;
}
.workspace-trust-intro-dialog .workspace-trust-dialog-image-row p {
display: flex;
align-items: center;
}
.workspace-trust-intro-dialog .workspace-trust-dialog-image-row.badge-row img {
max-height: 40px;
padding-right: 10px;
}
.workspace-trust-intro-dialog .workspace-trust-dialog-image-row.status-bar img {
max-height: 32px;
padding-right: 10px;
}
@@ -23,7 +23,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { ExtensionWorkspaceTrustRequestType } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IPromptChoiceWithMenu } from 'vs/platform/notification/common/notification';
import { IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification';
import { Link } from 'vs/platform/opener/browser/link';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -392,8 +392,8 @@ export class WorkspaceTrustEditor extends EditorPane {
const primaryButton = localize('workspaceTrustTransitionPrimaryButton', "Yes");
const secondaryButton = localize('workspaceTrustTransitionSecondaryButton', "No");
const result = await this.dialogService.confirm({ type: 'info', message, detail, primaryButton, secondaryButton });
if (!result.confirmed) {
const result = await this.dialogService.show(Severity.Info, message, [primaryButton, secondaryButton], { cancelId: 1, detail, custom: { icon: Codicon.shield } });
if (result.choice !== 0) {
return;
}
}
@@ -20,6 +20,7 @@ import { NativeDialogHandler } from 'vs/workbench/electron-sandbox/parts/dialogs
import { DialogService } from 'vs/workbench/services/dialogs/common/dialogService';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class DialogHandlerContribution extends Disposable implements IWorkbenchContribution {
private nativeImpl: IDialogHandler;
@@ -35,13 +36,14 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC
@ILayoutService layoutService: ILayoutService,
@IThemeService themeService: IThemeService,
@IKeybindingService keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IProductService productService: IProductService,
@IClipboardService clipboardService: IClipboardService,
@INativeHostService nativeHostService: INativeHostService
) {
super();
this.browserImpl = new BrowserDialogHandler(logService, layoutService, themeService, keybindingService, productService, clipboardService);
this.browserImpl = new BrowserDialogHandler(logService, layoutService, themeService, keybindingService, instantiationService, productService, clipboardService);
this.nativeImpl = new NativeDialogHandler(logService, nativeHostService, productService, clipboardService);
this.model = (this.dialogService as DialogService).model;
@@ -76,7 +78,7 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC
// Message
else if (this.currentDialog.args.showArgs) {
const args = this.currentDialog.args.showArgs;
result = this.useCustomDialog || args.options?.useCustom ?
result = (this.useCustomDialog || args.options?.custom) ?
await this.browserImpl.show(args.severity, args.message, args.buttons, args.options) :
await this.nativeImpl.show(args.severity, args.message, args.buttons, args.options);
}