Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve profile preview in web #172530

Merged
merged 2 commits into from Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -3,6 +3,38 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

.profile-view-tree-container .customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .actions {
display: inherit;
}

.monaco-workbench .pane > .pane-body > .profile-view-message-container {
display: flex;
padding: 13px 20px 0px 20px;
box-sizing: border-box;
}

.monaco-workbench .pane > .pane-body > .profile-view-message-container p {
margin-block-start: 0em;
margin-block-end: 0em;
}

.monaco-workbench .pane > .pane-body > .profile-view-message-container a {
color: var(--vscode-textLink-foreground)
}

.monaco-workbench .pane > .pane-body > .profile-view-message-container a:hover {
text-decoration: underline;
color: var(--vscode-textLink-activeForeground)
}

.monaco-workbench .pane > .pane-body > .profile-view-message-container a:active {
color: var(--vscode-textLink-activeForeground)
}

.monaco-workbench .pane > .pane-body > .profile-view-message-container.hide {
display: none;
}

.monaco-workbench .pane > .pane-body > .profile-view-buttons-container {
display: flex;
flex-direction: column;
Expand Down
Expand Up @@ -32,7 +32,7 @@ import { TasksResource, TasksResourceTreeItem } from 'vs/workbench/services/user
import { ExtensionsResource, ExtensionsResourceExportTreeItem, ExtensionsResourceImportTreeItem, ExtensionsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource';
import { GlobalStateResource, GlobalStateResourceExportTreeItem, GlobalStateResourceImportTreeItem, GlobalStateResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/globalStateResource';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { Button, ButtonWithDropdown } from 'vs/base/browser/ui/button/button';
import { Button } from 'vs/base/browser/ui/button/button';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
Expand All @@ -44,7 +44,7 @@ import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
import { generateUuid } from 'vs/base/common/uuid';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { EditorsOrder } from 'vs/workbench/common/editor';
import { getErrorMessage } from 'vs/base/common/errors';
import { getErrorMessage, onUnexpectedError } from 'vs/base/common/errors';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IQuickInputService, QuickPickItem } from 'vs/platform/quickinput/common/quickInput';
Expand All @@ -68,6 +68,8 @@ import { Barrier } from 'vs/base/common/async';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';

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

let extensions = false;
if (profileTemplate.extensions) {
const result = await this.dialogService.confirm({
title: localize('preview profile', "Preview Profile"),
message: localize('apply extensions', "Would you like to apply the extensions from the profile you are previewing or go through them manually?"),
type: 'info',
primaryButton: localize('apply extensions automatically', "Apply Extensions"),
secondaryButton: localize('apply extensions manually', "Apply Extensions (Manually)"),
});
extensions = result.confirmed;
}

const importedProfile = await this.importAndSwitch(profileTemplate, true, extensions, localize('preview profile', "Preview Profile"));
const importedProfile = await this.importAndSwitch(profileTemplate, true, false, localize('preview profile', "Preview Profile"));

if (!importedProfile) {
return;
Expand All @@ -348,55 +338,53 @@ export class UserDataProfileImportExportService extends Disposable implements IU
const barrier = new Barrier();
const importAction = this.getImportAction(barrier, userDataProfileImportState);
const secondaryAction = isWeb
? 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 }))
? 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 }))
: new BarrierAction(barrier, new Action('close', localize('close', "Close")));

const view = await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, importedProfile.name, [importAction], secondaryAction, false, userDataProfileImportState);
if (!extensions) {
userDataProfileImportState.setDescription(ProfileResourceType.Extensions, localize('not applied', "Not Applied"));
const that = this;
const disposable = disposables.add(registerAction2(class extends Action2 {
constructor() {
super({
id: 'previewProfile.applyExtensions',
title: localize('apply extensions title', "Apply Extensions"),
icon: Codicon.cloudDownload,
menu: {
id: MenuId.ViewItemContext,
group: 'inline',
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', IMPORT_PROFILE_PREVIEW_VIEW), ContextKeyExpr.equals('viewItem', ProfileResourceType.Extensions)),
}
});
}
override async run(): Promise<void> {
return that.progressService.withProgress({
location: IMPORT_PROFILE_PREVIEW_VIEW,
}, async progress => {
disposable.dispose();
userDataProfileImportState.setDescription(ProfileResourceType.Extensions, localize('applying', "Applying..."));
view.refresh();
const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
if (profileTemplate.extensions) {
await that.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, importedProfile);
userDataProfileImportState.setDescription(ProfileResourceType.Extensions, undefined);
await view.refresh();
}
});
}
}));
disposables.add(Event.debounce(this.extensionManagementService.onDidInstallExtensions, () => undefined, 100)(async () => {
const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
if (profileTemplate.extensions) {
const profileExtensions = await that.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(profileTemplate.extensions!);
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
if (profileExtensions.every(e => installed.some(i => areSameExtensions(e.identifier, i.identifier)))) {
disposable.dispose();
userDataProfileImportState.setDescription(ProfileResourceType.Extensions, undefined);
await view.refresh();
const view = await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, importedProfile.name, importAction, secondaryAction, false, userDataProfileImportState);
const message = new MarkdownString();
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. "));
message.appendMarkdown(`[${localize('learn more', "Learn more")}](https://aka.ms/vscode-extension-marketplace#_can-i-trust-extensions-from-the-marketplace).`);
view.setMessage(message);

const that = this;
const disposable = disposables.add(registerAction2(class extends Action2 {
constructor() {
super({
id: 'previewProfile.installExtensions',
title: localize('install extensions title', "Install Extensions"),
icon: Codicon.cloudDownload,
menu: {
id: MenuId.ViewItemContext,
group: 'inline',
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', IMPORT_PROFILE_PREVIEW_VIEW), ContextKeyExpr.equals('viewItem', ProfileResourceType.Extensions)),
}
});
}
override async run(): Promise<void> {
return that.progressService.withProgress({
location: IMPORT_PROFILE_PREVIEW_VIEW,
}, async progress => {
disposable.dispose();
view.setMessage(undefined);
const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
if (profileTemplate.extensions) {
await that.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, importedProfile);
}
});
}
}));
disposables.add(Event.debounce(this.extensionManagementService.onDidInstallExtensions, () => undefined, 100)(async () => {
const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
if (profileTemplate.extensions) {
const profileExtensions = await that.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(profileTemplate.extensions!);
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
if (profileExtensions.every(e => installed.some(i => areSameExtensions(e.identifier, i.identifier)))) {
disposable.dispose();
}
}));
}
}
}));

await barrier.wait();
await this.hideProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW);
} finally {
Expand All @@ -414,7 +402,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
if (userDataProfileImportState.isEmpty()) {
await importAction.run();
} else {
await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, profileTemplate.name, [importAction], new BarrierAction(barrier, new Action('cancel', localize('cancel', "Cancel"))), false, userDataProfileImportState);
await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, profileTemplate.name, importAction, new BarrierAction(barrier, new Action('cancel', localize('cancel', "Cancel"))), false, userDataProfileImportState);
}
await barrier.wait();
await this.hideProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW);
Expand All @@ -424,7 +412,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
}

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

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

private buttonsContainer!: HTMLElement;
private confirmButton!: Button | ButtonWithDropdown;
private cancelButton!: Button;
private primaryButton!: Button;
private secondaryButton!: Button;
private messageContainer!: HTMLElement;
private dimension: DOM.Dimension | undefined;
private totalTreeItemsCount: number = 0;

constructor(
private readonly userDataProfileData: UserDataProfileImportExportState,
private readonly confirmActions: Action[],
private readonly cancelAction: Action,
private readonly primaryAction: Action,
private readonly secondaryAction: Action,
private readonly actionRunner: IActionRunner,
options: IViewletViewOptions,
@IKeybindingService keybindingService: IKeybindingService,
Expand All @@ -732,7 +721,8 @@ class UserDataProfilePreviewViewPane extends TreeViewPane {

protected override renderTreeView(container: HTMLElement): void {
this.treeView.dataProvider = this.userDataProfileData;
super.renderTreeView(DOM.append(container, DOM.$('')));
super.renderTreeView(DOM.append(container, DOM.$('.profile-view-tree-container')));
this.messageContainer = DOM.append(container, DOM.$('.profile-view-message-container.hide'));
this.createButtons(container);
this._register(this.treeView.onDidChangeCheckboxState(items => {
this.treeView.refresh(this.userDataProfileData.onDidChangeCheckboxState(items));
Expand Down Expand Up @@ -765,42 +755,64 @@ class UserDataProfilePreviewViewPane extends TreeViewPane {
private createButtons(container: HTMLElement): void {
this.buttonsContainer = DOM.append(container, DOM.$('.profile-view-buttons-container'));

this.confirmButton = this._register(this.confirmActions.length > 1
? new ButtonWithDropdown(this.buttonsContainer, { ...defaultButtonStyles, actions: this.confirmActions.slice(1), contextMenuProvider: this.contextMenuService, actionRunner: this.actionRunner, addPrimaryActionToDropdown: false })
: new Button(this.buttonsContainer, { ...defaultButtonStyles }));
this.confirmButton.element.classList.add('profile-view-button');
this.confirmButton.label = this.confirmActions[0].label;
this.confirmButton.enabled = this.confirmActions[0].enabled;
this._register(this.confirmButton.onDidClick(() => this.actionRunner.run(this.confirmActions[0])));
this._register(this.confirmActions[0].onDidChange(e => {
this.primaryButton = this._register(new Button(this.buttonsContainer, { ...defaultButtonStyles }));
this.primaryButton.element.classList.add('profile-view-button');
this.primaryButton.label = this.primaryAction.label;
this.primaryButton.enabled = this.primaryAction.enabled;
this._register(this.primaryButton.onDidClick(() => this.actionRunner.run(this.primaryAction)));
this._register(this.primaryAction.onDidChange(e => {
if (e.enabled !== undefined) {
this.confirmButton.enabled = e.enabled;
this.primaryButton.enabled = e.enabled;
}
}));

this.cancelButton = this._register(new Button(this.buttonsContainer, { secondary: true, ...defaultButtonStyles }));
this.cancelButton.label = this.cancelAction.label;
this.cancelButton.element.classList.add('profile-view-button');
this.cancelButton.enabled = this.cancelAction.enabled;
this._register(this.cancelButton.onDidClick(() => this.actionRunner.run(this.cancelAction)));
this._register(this.cancelAction.onDidChange(e => {
this.secondaryButton = this._register(new Button(this.buttonsContainer, { secondary: true, ...defaultButtonStyles }));
this.secondaryButton.label = this.secondaryAction.label;
this.secondaryButton.element.classList.add('profile-view-button');
this.secondaryButton.enabled = this.secondaryAction.enabled;
this._register(this.secondaryButton.onDidClick(() => this.actionRunner.run(this.secondaryAction)));
this._register(this.secondaryAction.onDidChange(e => {
if (e.enabled !== undefined) {
this.cancelButton.enabled = e.enabled;
this.secondaryButton.enabled = e.enabled;
}
}));
}

protected override layoutTreeView(height: number, width: number): void {
this.dimension = new DOM.Dimension(width, height);

let messageContainerHeight = 0;
if (!this.messageContainer.classList.contains('hide')) {
messageContainerHeight = DOM.getClientArea(this.messageContainer).height;
}

const buttonContainerHeight = 108;
this.buttonsContainer.style.height = `${buttonContainerHeight}px`;
this.buttonsContainer.style.width = `${width}px`;

super.layoutTreeView(Math.min(height - buttonContainerHeight, 22 * this.totalTreeItemsCount), width);
super.layoutTreeView(Math.min(height - buttonContainerHeight - messageContainerHeight, 22 * this.totalTreeItemsCount), width);
}

private updateConfirmButtonEnablement(): void {
this.confirmButton.enabled = this.confirmActions[0].enabled && this.userDataProfileData.isEnabled();
this.primaryButton.enabled = this.primaryAction.enabled && this.userDataProfileData.isEnabled();
}

private readonly renderDisposables = this._register(new DisposableStore());
setMessage(message: MarkdownString | undefined): void {
this.messageContainer.classList.toggle('hide', !message);
DOM.clearNode(this.messageContainer);
if (message) {
this.renderDisposables.clear();
const rendered = this.renderDisposables.add(renderMarkdown(message, {
actionHandler: {
callback: (content) => {
this.openerService.open(content, { allowCommands: true }).catch(onUnexpectedError);
},
disposables: this.renderDisposables
}
}));
DOM.append(this.messageContainer, rendered.element);
}
}

refresh(): Promise<void> {
Expand Down