Skip to content

Commit d132035

Browse files
authored
Improve profile preview in web (microsoft#172530)
* profile preview imporvements - remove dialog - introduce message * support rendering markdown message
1 parent 9c5f7ec commit d132035

File tree

2 files changed

+131
-87
lines changed

2 files changed

+131
-87
lines changed

src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,38 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
.profile-view-tree-container .customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .actions {
7+
display: inherit;
8+
}
9+
10+
.monaco-workbench .pane > .pane-body > .profile-view-message-container {
11+
display: flex;
12+
padding: 13px 20px 0px 20px;
13+
box-sizing: border-box;
14+
}
15+
16+
.monaco-workbench .pane > .pane-body > .profile-view-message-container p {
17+
margin-block-start: 0em;
18+
margin-block-end: 0em;
19+
}
20+
21+
.monaco-workbench .pane > .pane-body > .profile-view-message-container a {
22+
color: var(--vscode-textLink-foreground)
23+
}
24+
25+
.monaco-workbench .pane > .pane-body > .profile-view-message-container a:hover {
26+
text-decoration: underline;
27+
color: var(--vscode-textLink-activeForeground)
28+
}
29+
30+
.monaco-workbench .pane > .pane-body > .profile-view-message-container a:active {
31+
color: var(--vscode-textLink-activeForeground)
32+
}
33+
34+
.monaco-workbench .pane > .pane-body > .profile-view-message-container.hide {
35+
display: none;
36+
}
37+
638
.monaco-workbench .pane > .pane-body > .profile-view-buttons-container {
739
display: flex;
840
flex-direction: column;

src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts

Lines changed: 99 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { TasksResource, TasksResourceTreeItem } from 'vs/workbench/services/user
3232
import { ExtensionsResource, ExtensionsResourceExportTreeItem, ExtensionsResourceImportTreeItem, ExtensionsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource';
3333
import { GlobalStateResource, GlobalStateResourceExportTreeItem, GlobalStateResourceImportTreeItem, GlobalStateResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/globalStateResource';
3434
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
35-
import { Button, ButtonWithDropdown } from 'vs/base/browser/ui/button/button';
35+
import { Button } from 'vs/base/browser/ui/button/button';
3636
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
3737
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
3838
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
@@ -44,7 +44,7 @@ import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
4444
import { generateUuid } from 'vs/base/common/uuid';
4545
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
4646
import { EditorsOrder } from 'vs/workbench/common/editor';
47-
import { getErrorMessage } from 'vs/base/common/errors';
47+
import { getErrorMessage, onUnexpectedError } from 'vs/base/common/errors';
4848
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
4949
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
5050
import { IQuickInputService, QuickPickItem } from 'vs/platform/quickinput/common/quickInput';
@@ -68,6 +68,8 @@ import { Barrier } from 'vs/base/common/async';
6868
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
6969
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
7070
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
71+
import { MarkdownString } from 'vs/base/common/htmlContent';
72+
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
7173

7274
interface IUserDataProfileTemplate {
7375
readonly name: string;
@@ -231,7 +233,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
231233
return this.doExportProfile(userDataProfilesExportState);
232234
}));
233235
const closeAction = new BarrierAction(barrier, new Action('close', localize('close', "Close")));
234-
await this.showProfilePreviewView(EXPORT_PROFILE_PREVIEW_VIEW, userDataProfilesExportState.profile.name, [exportAction], closeAction, true, userDataProfilesExportState);
236+
await this.showProfilePreviewView(EXPORT_PROFILE_PREVIEW_VIEW, userDataProfilesExportState.profile.name, exportAction, closeAction, true, userDataProfilesExportState);
235237
disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(e => barrier.open()));
236238
await barrier.wait();
237239
await this.hideProfilePreviewView(EXPORT_PROFILE_PREVIEW_VIEW);
@@ -327,19 +329,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
327329
const userDataProfileImportState = disposables.add(this.instantiationService.createInstance(UserDataProfileImportState, profileTemplate));
328330
profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
329331

330-
let extensions = false;
331-
if (profileTemplate.extensions) {
332-
const result = await this.dialogService.confirm({
333-
title: localize('preview profile', "Preview Profile"),
334-
message: localize('apply extensions', "Would you like to apply the extensions from the profile you are previewing or go through them manually?"),
335-
type: 'info',
336-
primaryButton: localize('apply extensions automatically', "Apply Extensions"),
337-
secondaryButton: localize('apply extensions manually', "Apply Extensions (Manually)"),
338-
});
339-
extensions = result.confirmed;
340-
}
341-
342-
const importedProfile = await this.importAndSwitch(profileTemplate, true, extensions, localize('preview profile', "Preview Profile"));
332+
const importedProfile = await this.importAndSwitch(profileTemplate, true, false, localize('preview profile', "Preview Profile"));
343333

344334
if (!importedProfile) {
345335
return;
@@ -348,55 +338,53 @@ export class UserDataProfileImportExportService extends Disposable implements IU
348338
const barrier = new Barrier();
349339
const importAction = this.getImportAction(barrier, userDataProfileImportState);
350340
const secondaryAction = isWeb
351-
? new Action('importInDesktop', localize('import in desktop', "Import {0} profile in {1}", importedProfile.name, this.productService.nameLong), undefined, true, async () => this.openerService.open(uri, { openExternal: true }))
341+
? new Action('importInDesktop', localize('import in desktop', "Import Profile in {1}", importedProfile.name, this.productService.nameLong), undefined, true, async () => this.openerService.open(uri, { openExternal: true }))
352342
: new BarrierAction(barrier, new Action('close', localize('close', "Close")));
353343

354-
const view = await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, importedProfile.name, [importAction], secondaryAction, false, userDataProfileImportState);
355-
if (!extensions) {
356-
userDataProfileImportState.setDescription(ProfileResourceType.Extensions, localize('not applied', "Not Applied"));
357-
const that = this;
358-
const disposable = disposables.add(registerAction2(class extends Action2 {
359-
constructor() {
360-
super({
361-
id: 'previewProfile.applyExtensions',
362-
title: localize('apply extensions title', "Apply Extensions"),
363-
icon: Codicon.cloudDownload,
364-
menu: {
365-
id: MenuId.ViewItemContext,
366-
group: 'inline',
367-
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', IMPORT_PROFILE_PREVIEW_VIEW), ContextKeyExpr.equals('viewItem', ProfileResourceType.Extensions)),
368-
}
369-
});
370-
}
371-
override async run(): Promise<void> {
372-
return that.progressService.withProgress({
373-
location: IMPORT_PROFILE_PREVIEW_VIEW,
374-
}, async progress => {
375-
disposable.dispose();
376-
userDataProfileImportState.setDescription(ProfileResourceType.Extensions, localize('applying', "Applying..."));
377-
view.refresh();
378-
const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
379-
if (profileTemplate.extensions) {
380-
await that.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, importedProfile);
381-
userDataProfileImportState.setDescription(ProfileResourceType.Extensions, undefined);
382-
await view.refresh();
383-
}
384-
});
385-
}
386-
}));
387-
disposables.add(Event.debounce(this.extensionManagementService.onDidInstallExtensions, () => undefined, 100)(async () => {
388-
const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
389-
if (profileTemplate.extensions) {
390-
const profileExtensions = await that.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(profileTemplate.extensions!);
391-
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
392-
if (profileExtensions.every(e => installed.some(i => areSameExtensions(e.identifier, i.identifier)))) {
393-
disposable.dispose();
394-
userDataProfileImportState.setDescription(ProfileResourceType.Extensions, undefined);
395-
await view.refresh();
344+
const view = await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, importedProfile.name, importAction, secondaryAction, false, userDataProfileImportState);
345+
const message = new MarkdownString();
346+
message.appendMarkdown(localize('preview profile message', "By default, extensions aren't installed when previewing a profile on the web. You can still install them manually before importing the profile. "));
347+
message.appendMarkdown(`[${localize('learn more', "Learn more")}](https://aka.ms/vscode-extension-marketplace#_can-i-trust-extensions-from-the-marketplace).`);
348+
view.setMessage(message);
349+
350+
const that = this;
351+
const disposable = disposables.add(registerAction2(class extends Action2 {
352+
constructor() {
353+
super({
354+
id: 'previewProfile.installExtensions',
355+
title: localize('install extensions title', "Install Extensions"),
356+
icon: Codicon.cloudDownload,
357+
menu: {
358+
id: MenuId.ViewItemContext,
359+
group: 'inline',
360+
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', IMPORT_PROFILE_PREVIEW_VIEW), ContextKeyExpr.equals('viewItem', ProfileResourceType.Extensions)),
361+
}
362+
});
363+
}
364+
override async run(): Promise<void> {
365+
return that.progressService.withProgress({
366+
location: IMPORT_PROFILE_PREVIEW_VIEW,
367+
}, async progress => {
368+
disposable.dispose();
369+
view.setMessage(undefined);
370+
const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
371+
if (profileTemplate.extensions) {
372+
await that.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, importedProfile);
396373
}
374+
});
375+
}
376+
}));
377+
disposables.add(Event.debounce(this.extensionManagementService.onDidInstallExtensions, () => undefined, 100)(async () => {
378+
const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
379+
if (profileTemplate.extensions) {
380+
const profileExtensions = await that.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(profileTemplate.extensions!);
381+
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
382+
if (profileExtensions.every(e => installed.some(i => areSameExtensions(e.identifier, i.identifier)))) {
383+
disposable.dispose();
397384
}
398-
}));
399-
}
385+
}
386+
}));
387+
400388
await barrier.wait();
401389
await this.hideProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW);
402390
} finally {
@@ -414,7 +402,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
414402
if (userDataProfileImportState.isEmpty()) {
415403
await importAction.run();
416404
} else {
417-
await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, profileTemplate.name, [importAction], new BarrierAction(barrier, new Action('cancel', localize('cancel', "Cancel"))), false, userDataProfileImportState);
405+
await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, profileTemplate.name, importAction, new BarrierAction(barrier, new Action('cancel', localize('cancel', "Cancel"))), false, userDataProfileImportState);
418406
}
419407
await barrier.wait();
420408
await this.hideProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW);
@@ -424,7 +412,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
424412
}
425413

426414
private getImportAction(barrier: Barrier, userDataProfileImportState: UserDataProfileImportState): IAction {
427-
const title = localize('import', "Import {0} profile", userDataProfileImportState.profile.name);
415+
const title = localize('import', "Import Profile", userDataProfileImportState.profile.name);
428416
const importAction = new BarrierAction(barrier, new Action('import', title, undefined, true, () => {
429417
const importProfileFn = async () => {
430418
importAction.enabled = false;
@@ -599,7 +587,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
599587
return nameIndex + 1;
600588
}
601589

602-
private async showProfilePreviewView(id: string, name: string, primary: IAction[], secondary: IAction, refreshAction: boolean, userDataProfilesData: UserDataProfileImportExportState): Promise<UserDataProfilePreviewViewPane> {
590+
private async showProfilePreviewView(id: string, name: string, primary: IAction, secondary: IAction, refreshAction: boolean, userDataProfilesData: UserDataProfileImportExportState): Promise<UserDataProfilePreviewViewPane> {
603591
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
604592
const treeView = this.instantiationService.createInstance(TreeView, id, name);
605593
if (refreshAction) {
@@ -705,15 +693,16 @@ class FileUserDataProfileContentHandler implements IUserDataProfileContentHandle
705693
class UserDataProfilePreviewViewPane extends TreeViewPane {
706694

707695
private buttonsContainer!: HTMLElement;
708-
private confirmButton!: Button | ButtonWithDropdown;
709-
private cancelButton!: Button;
696+
private primaryButton!: Button;
697+
private secondaryButton!: Button;
698+
private messageContainer!: HTMLElement;
710699
private dimension: DOM.Dimension | undefined;
711700
private totalTreeItemsCount: number = 0;
712701

713702
constructor(
714703
private readonly userDataProfileData: UserDataProfileImportExportState,
715-
private readonly confirmActions: Action[],
716-
private readonly cancelAction: Action,
704+
private readonly primaryAction: Action,
705+
private readonly secondaryAction: Action,
717706
private readonly actionRunner: IActionRunner,
718707
options: IViewletViewOptions,
719708
@IKeybindingService keybindingService: IKeybindingService,
@@ -732,7 +721,8 @@ class UserDataProfilePreviewViewPane extends TreeViewPane {
732721

733722
protected override renderTreeView(container: HTMLElement): void {
734723
this.treeView.dataProvider = this.userDataProfileData;
735-
super.renderTreeView(DOM.append(container, DOM.$('')));
724+
super.renderTreeView(DOM.append(container, DOM.$('.profile-view-tree-container')));
725+
this.messageContainer = DOM.append(container, DOM.$('.profile-view-message-container.hide'));
736726
this.createButtons(container);
737727
this._register(this.treeView.onDidChangeCheckboxState(items => {
738728
this.treeView.refresh(this.userDataProfileData.onDidChangeCheckboxState(items));
@@ -765,42 +755,64 @@ class UserDataProfilePreviewViewPane extends TreeViewPane {
765755
private createButtons(container: HTMLElement): void {
766756
this.buttonsContainer = DOM.append(container, DOM.$('.profile-view-buttons-container'));
767757

768-
this.confirmButton = this._register(this.confirmActions.length > 1
769-
? new ButtonWithDropdown(this.buttonsContainer, { ...defaultButtonStyles, actions: this.confirmActions.slice(1), contextMenuProvider: this.contextMenuService, actionRunner: this.actionRunner, addPrimaryActionToDropdown: false })
770-
: new Button(this.buttonsContainer, { ...defaultButtonStyles }));
771-
this.confirmButton.element.classList.add('profile-view-button');
772-
this.confirmButton.label = this.confirmActions[0].label;
773-
this.confirmButton.enabled = this.confirmActions[0].enabled;
774-
this._register(this.confirmButton.onDidClick(() => this.actionRunner.run(this.confirmActions[0])));
775-
this._register(this.confirmActions[0].onDidChange(e => {
758+
this.primaryButton = this._register(new Button(this.buttonsContainer, { ...defaultButtonStyles }));
759+
this.primaryButton.element.classList.add('profile-view-button');
760+
this.primaryButton.label = this.primaryAction.label;
761+
this.primaryButton.enabled = this.primaryAction.enabled;
762+
this._register(this.primaryButton.onDidClick(() => this.actionRunner.run(this.primaryAction)));
763+
this._register(this.primaryAction.onDidChange(e => {
776764
if (e.enabled !== undefined) {
777-
this.confirmButton.enabled = e.enabled;
765+
this.primaryButton.enabled = e.enabled;
778766
}
779767
}));
780768

781-
this.cancelButton = this._register(new Button(this.buttonsContainer, { secondary: true, ...defaultButtonStyles }));
782-
this.cancelButton.label = this.cancelAction.label;
783-
this.cancelButton.element.classList.add('profile-view-button');
784-
this.cancelButton.enabled = this.cancelAction.enabled;
785-
this._register(this.cancelButton.onDidClick(() => this.actionRunner.run(this.cancelAction)));
786-
this._register(this.cancelAction.onDidChange(e => {
769+
this.secondaryButton = this._register(new Button(this.buttonsContainer, { secondary: true, ...defaultButtonStyles }));
770+
this.secondaryButton.label = this.secondaryAction.label;
771+
this.secondaryButton.element.classList.add('profile-view-button');
772+
this.secondaryButton.enabled = this.secondaryAction.enabled;
773+
this._register(this.secondaryButton.onDidClick(() => this.actionRunner.run(this.secondaryAction)));
774+
this._register(this.secondaryAction.onDidChange(e => {
787775
if (e.enabled !== undefined) {
788-
this.cancelButton.enabled = e.enabled;
776+
this.secondaryButton.enabled = e.enabled;
789777
}
790778
}));
791779
}
792780

793781
protected override layoutTreeView(height: number, width: number): void {
794782
this.dimension = new DOM.Dimension(width, height);
783+
784+
let messageContainerHeight = 0;
785+
if (!this.messageContainer.classList.contains('hide')) {
786+
messageContainerHeight = DOM.getClientArea(this.messageContainer).height;
787+
}
788+
795789
const buttonContainerHeight = 108;
796790
this.buttonsContainer.style.height = `${buttonContainerHeight}px`;
797791
this.buttonsContainer.style.width = `${width}px`;
798792

799-
super.layoutTreeView(Math.min(height - buttonContainerHeight, 22 * this.totalTreeItemsCount), width);
793+
super.layoutTreeView(Math.min(height - buttonContainerHeight - messageContainerHeight, 22 * this.totalTreeItemsCount), width);
800794
}
801795

802796
private updateConfirmButtonEnablement(): void {
803-
this.confirmButton.enabled = this.confirmActions[0].enabled && this.userDataProfileData.isEnabled();
797+
this.primaryButton.enabled = this.primaryAction.enabled && this.userDataProfileData.isEnabled();
798+
}
799+
800+
private readonly renderDisposables = this._register(new DisposableStore());
801+
setMessage(message: MarkdownString | undefined): void {
802+
this.messageContainer.classList.toggle('hide', !message);
803+
DOM.clearNode(this.messageContainer);
804+
if (message) {
805+
this.renderDisposables.clear();
806+
const rendered = this.renderDisposables.add(renderMarkdown(message, {
807+
actionHandler: {
808+
callback: (content) => {
809+
this.openerService.open(content, { allowCommands: true }).catch(onUnexpectedError);
810+
},
811+
disposables: this.renderDisposables
812+
}
813+
}));
814+
DOM.append(this.messageContainer, rendered.element);
815+
}
804816
}
805817

806818
refresh(): Promise<void> {

0 commit comments

Comments
 (0)