diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 32cc759dc7d0d6..25916c435f4121 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -13,6 +13,29 @@ export interface IBuiltInExtension { readonly metadata: any; } +export interface IProductWalkthrough { + id: string; + steps: IProductWalkthroughStep[]; +} + +export interface IProductWalkthroughStep { + id: string; + title: string; + when: string; + description: string; + media: + | { type: 'image'; path: string | { hc: string; hcLight?: string; light: string; dark: string }; altText: string } + | { type: 'svg'; path: string; altText: string } + | { type: 'markdown'; path: string }; +} + +export interface IFeaturedExtension { + readonly id: string; + readonly title: string; + readonly description: string; + readonly imagePath: string; +} + export type ConfigurationSyncStore = { url: string; insidersUrl: string; @@ -50,6 +73,8 @@ export interface IProductConfiguration { readonly dataFolderName: string; // location for extensions (e.g. ~/.vscode-insiders) readonly builtInExtensions?: IBuiltInExtension[]; + readonly walkthroughMetadata?: IProductWalkthrough[]; + readonly featuredExtensions?: IFeaturedExtension[]; readonly downloadUrl?: string; readonly updateUrl?: string; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService.ts new file mode 100644 index 00000000000000..8a9301528ae204 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService.ts @@ -0,0 +1,200 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IFeaturedExtension } from 'vs/base/common/product'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { localize } from 'vs/nls'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; + +type FeaturedExtensionTreatment = { extensions: string[]; showAsList?: string }; +type FeaturedExtensionStorageData = { title: string; description: string; imagePath: string; date: number }; + +export const IFeaturedExtensionsService = createDecorator('featuredExtensionsService'); + +export interface IFeaturedExtensionsService { + _serviceBrand: undefined; + + getExtensions(): Promise; + title: string; +} + +const enum FeaturedExtensionMetadataType { + Title, + Description, + ImagePath +} + +export class FeaturedExtensionsService extends Disposable implements IFeaturedExtensionsService { + declare readonly _serviceBrand: undefined; + + private ignoredExtensions: Set = new Set(); + private treatment: FeaturedExtensionTreatment | undefined; + private _isInitialized: boolean = false; + + private static readonly STORAGE_KEY = 'workbench.welcomePage.extensionMetadata'; + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService, + @IStorageService private readonly storageService: IStorageService, + @IProductService private readonly productService: IProductService, + @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + ) { + super(); + this.title = localize('gettingStarted.featuredTitle', 'Featured'); + } + + title: string; + + async getExtensions(): Promise { + + await this._init(); + + let treatments = this.treatment?.extensions.filter(extension => !this.ignoredExtensions.has(extension)) ?? new Array(); + const featuredExtensions: IFeaturedExtension[] = new Array(); + + if (this.treatment?.showAsList !== 'true' && treatments.length > 0) { + // pick a random extensionId for display + const treatment = treatments[Math.floor(Math.random() * treatments.length)]; + treatments = [treatment]; + } + + for (const treatment of treatments) { + const extension = await this.resolveExtension(treatment); + if (extension) { + featuredExtensions.push(extension); + } + } + + return featuredExtensions; + } + + private async _init(): Promise { + + if (this._isInitialized) { + return; + } + + const extensions = await Promise.race([ + this.tasExperimentService?.getTreatment('welcome.featured.item'), + new Promise(resolve => setTimeout(() => resolve(''), 2000)) + ]); + + const extensionListTitle = await Promise.race([ + this.tasExperimentService?.getTreatment('welcome.featured.title'), + new Promise(resolve => setTimeout(() => resolve(''), 2000)) + ]); + + this.treatment = extensions ? JSON.parse(extensions) : { extensions: [] }; + this.title = extensionListTitle ?? localize('gettingStarted.featuredTitle', 'Featured'); + + if (this.treatment) { + const installed = await this.extensionManagementService.getInstalled(); + + for (const extension of this.treatment.extensions) { + if (installed.some(e => ExtensionIdentifier.equals(e.identifier.id, extension))) { + this.ignoredExtensions.add(extension); + } + } + } + + this._isInitialized = true; + } + + private async resolveExtension(extensionId: string): Promise { + + const productMetadata = this.productService.featuredExtensions?.find(e => ExtensionIdentifier.equals(e.id, extensionId)); + + const title = productMetadata?.title ?? await this.getMetadata(extensionId, FeaturedExtensionMetadataType.Title); + const description = productMetadata?.description ?? await this.getMetadata(extensionId, FeaturedExtensionMetadataType.Description); + const imagePath = productMetadata?.imagePath ?? await this.getMetadata(extensionId, FeaturedExtensionMetadataType.ImagePath); + + if (title && description && imagePath) { + return { + id: extensionId, + title: title, + description: description, + imagePath: imagePath, + }; + } + return undefined; + } + + private async getMetadata(extensionId: string, key: FeaturedExtensionMetadataType): Promise { + + const storageMetadata = this.getStorageData(extensionId); + if (storageMetadata) { + switch (key) { + case FeaturedExtensionMetadataType.Title: { + return storageMetadata.title; + } + case FeaturedExtensionMetadataType.Description: { + return storageMetadata.description; + } + case FeaturedExtensionMetadataType.ImagePath: { + return storageMetadata.imagePath; + } + default: + return undefined; + } + } + + return await this.getGalleryMetadata(extensionId, key); + } + + private getStorageData(extensionId: string): FeaturedExtensionStorageData | undefined { + const metadata = this.storageService.get(FeaturedExtensionsService.STORAGE_KEY + '.' + extensionId, StorageScope.APPLICATION); + if (metadata) { + const value = JSON.parse(metadata) as FeaturedExtensionStorageData; + const lastUpdateDate = new Date().getTime() - value.date; + if (lastUpdateDate < 1000 * 60 * 60 * 24 * 7) { + return value; + } + } + return undefined; + } + + private async getGalleryMetadata(extensionId: string, key: FeaturedExtensionMetadataType): Promise { + + const storageKey = FeaturedExtensionsService.STORAGE_KEY + '.' + extensionId; + this.storageService.remove(storageKey, StorageScope.APPLICATION); + + const galleryExtension = (await this.galleryService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; + let metadata: string | undefined; + if (galleryExtension) { + switch (key) { + case FeaturedExtensionMetadataType.Title: { + metadata = galleryExtension.displayName; + break; + } + case FeaturedExtensionMetadataType.Description: { + metadata = galleryExtension.description; + break; + } + case FeaturedExtensionMetadataType.ImagePath: { + metadata = galleryExtension.assets.icon?.uri; + break; + } + } + + this.storageService.store(storageKey, JSON.stringify({ + title: galleryExtension.displayName, + description: galleryExtension.description, + imagePath: galleryExtension.assets.icon?.uri, + date: new Date().getTime() + }), StorageScope.APPLICATION, StorageTarget.MACHINE); + } + return metadata; + } +} + +registerSingleton(IFeaturedExtensionsService, FeaturedExtensionsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index bf09fb17c71cc6..8254eee27688f1 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -70,6 +70,10 @@ import { GettingStartedDetailsRenderer } from 'vs/workbench/contrib/welcomeGetti import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { defaultButtonStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IFeaturedExtensionsService } from 'vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService'; +import { IFeaturedExtension } from 'vs/base/common/product'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -98,6 +102,13 @@ const parsedStartEntries: IWelcomePageStartEntry[] = startEntries.map((e, i) => when: ContextKeyExpr.deserialize(e.when) ?? ContextKeyExpr.true() })); +type GettingStartedLayoutEventClassification = { + owner: 'bhavyau'; + comment: 'Information about the layout of the welcome page'; + featuredExtensions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'visible featured extensions' }; + title: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'featured extension title' }; +}; + type GettingStartedActionClassification = { command: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The command being executed on the getting started page.' }; walkthroughId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The walkthrough which the command is in' }; @@ -127,6 +138,8 @@ export class GettingStartedPage extends EditorPane { private detailsPageDisposables: DisposableStore = new DisposableStore(); private gettingStartedCategories: IResolvedWalkthrough[]; + private featuredExtensions?: Promise; + private currentWalkthrough: IResolvedWalkthrough | undefined; private categoriesPageScrollbar: DomScrollableElement | undefined; @@ -145,6 +158,7 @@ export class GettingStartedPage extends EditorPane { private recentlyOpenedList?: GettingStartedIndexList; private startList?: GettingStartedIndexList; private gettingStartedList?: GettingStartedIndexList; + private featuredExtensionsList?: GettingStartedIndexList; private stepsSlide!: HTMLElement; private categoriesSlide!: HTMLElement; @@ -162,6 +176,7 @@ export class GettingStartedPage extends EditorPane { @IProductService private readonly productService: IProductService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IWalkthroughsService private readonly gettingStartedService: IWalkthroughsService, + @IFeaturedExtensionsService private readonly featuredExtensionService: IFeaturedExtensionsService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService telemetryService: ITelemetryService, @ILanguageService private readonly languageService: ILanguageService, @@ -181,6 +196,7 @@ export class GettingStartedPage extends EditorPane { @IWebviewService private readonly webviewService: IWebviewService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService ) { super(GettingStartedPage.ID, telemetryService, themeService, storageService); @@ -203,11 +219,15 @@ export class GettingStartedPage extends EditorPane { embedderIdentifierContext.bindTo(this.contextService).set(productService.embedderIdentifier); this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); + this.featuredExtensions = this.featuredExtensionService.getExtensions(); + this._register(this.dispatchListeners); this.buildSlideThrottle = new Throttler(); const rerender = () => { this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); + this.featuredExtensions = this.featuredExtensionService.getExtensions(); + if (this.currentWalkthrough) { const existingSteps = this.currentWalkthrough.steps.map(step => step.id); const newCategory = this.gettingStartedCategories.find(category => this.currentWalkthrough?.id === category.id); @@ -222,6 +242,15 @@ export class GettingStartedPage extends EditorPane { } }; + this._register(this.extensionManagementService.onDidInstallExtensions(async (result) => { + for (const e of result) { + const installedFeaturedExtension = (await this.featuredExtensions)?.find(ext => ExtensionIdentifier.equals(ext.id, e.identifier.id)); + if (installedFeaturedExtension) { + this.hideExtension(e.identifier.id); + } + } + })); + this._register(this.gettingStartedService.onDidAddWalkthrough(rerender)); this._register(this.gettingStartedService.onDidRemoveWalkthrough(rerender)); @@ -420,6 +449,14 @@ export class GettingStartedPage extends EditorPane { } break; } + case 'openExtensionPage': { + this.commandService.executeCommand('extension.open', argument); + break; + } + case 'hideExtension': { + this.hideExtension(argument); + break; + } default: { console.error('Dispatch to', command, argument, 'not defined'); break; @@ -434,6 +471,12 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.rerender(); } + private hideExtension(extensionId: string) { + this.setHiddenCategories([...this.getHiddenCategories().add(extensionId)]); + this.featuredExtensionsList?.rerender(); + this.registerDispatchListeners(); + } + private markAllStepsComplete() { if (this.currentWalkthrough) { this.currentWalkthrough?.steps.forEach(step => { @@ -721,7 +764,6 @@ export class GettingStartedPage extends EditorPane { this.categoriesPageScrollbar.scanDomNode(); this.detailsPageScrollbar.scanDomNode(); - parent.appendChild(this.container); } @@ -759,12 +801,12 @@ export class GettingStartedPage extends EditorPane { $('p.subtitle.description', {}, localize({ key: 'gettingStarted.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "Editing evolved")) ); - const leftColumn = $('.categories-column.categories-column-left', {},); const rightColumn = $('.categories-column.categories-column-right', {},); const startList = this.buildStartList(); const recentList = this.buildRecentlyOpenedList(); + const featuredExtensionList = this.buildFeaturedExtensionsList(); const gettingStartedList = this.buildGettingStartedWalkthroughsList(); const footer = $('.footer', {}, @@ -777,18 +819,36 @@ export class GettingStartedPage extends EditorPane { if (gettingStartedList.itemCount) { this.container.classList.remove('noWalkthroughs'); reset(leftColumn, startList.getDomElement(), recentList.getDomElement()); - reset(rightColumn, gettingStartedList.getDomElement()); + reset(rightColumn, featuredExtensionList.getDomElement(), gettingStartedList.getDomElement()); recentList.setLimit(5); } else { this.container.classList.add('noWalkthroughs'); reset(leftColumn, startList.getDomElement()); - reset(rightColumn, recentList.getDomElement()); + reset(rightColumn, recentList.getDomElement(), featuredExtensionList.getDomElement()); + recentList.setLimit(10); + } + setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); + }; + + const layoutFeaturedExtension = () => { + if (featuredExtensionList.itemCount) { + this.container.classList.remove('noExtensions'); + reset(leftColumn, startList.getDomElement(), recentList.getDomElement()); + reset(rightColumn, featuredExtensionList.getDomElement(), gettingStartedList.getDomElement()); + recentList.setLimit(5); + } + else { + this.container.classList.add('noExtensions'); + reset(leftColumn, startList.getDomElement(), recentList.getDomElement()); + reset(rightColumn, gettingStartedList.getDomElement()); recentList.setLimit(10); } setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); }; + featuredExtensionList.onDidChange(layoutFeaturedExtension); + layoutFeaturedExtension(); gettingStartedList.onDidChange(layoutLists); layoutLists(); @@ -1041,10 +1101,65 @@ export class GettingStartedPage extends EditorPane { gettingStartedList.setEntries(this.gettingStartedCategories); allWalkthroughsHiddenContext.bindTo(this.contextService).set(gettingStartedList.itemCount === 0); - return gettingStartedList; } + private buildFeaturedExtensionsList(): GettingStartedIndexList { + + const renderFeaturedExtensions = (entry: IFeaturedExtension): HTMLElement => { + + const descriptionContent = $('.featured-description-content', {},); + + reset(descriptionContent, ...renderLabelWithIcons(entry.description)); + + const titleContent = $('h3.category-title.max-lines-3', { 'x-category-title-for': entry.id }); + reset(titleContent, ...renderLabelWithIcons(entry.title)); + + return $('button.getting-started-category', + { + 'x-dispatch': 'openExtensionPage:' + entry.id, + 'title': entry.description + }, + $('.main-content', {}, + $('img.featured-icon.icon-widget', { src: entry.imagePath }), + titleContent, + $('a.codicon.codicon-close.hide-category-button', { + 'tabindex': 0, + 'x-dispatch': 'hideExtension:' + entry.id, + 'title': localize('close', "Hide"), + 'role': 'button', + 'aria-label': localize('closeAriaLabel', "Hide"), + }), + ), + descriptionContent); + }; + + if (this.featuredExtensionsList) { + this.featuredExtensionsList.dispose(); + } + + const featuredExtensionsList = this.featuredExtensionsList = new GettingStartedIndexList( + { + title: this.featuredExtensionService.title, + klass: 'featured-extensions', + limit: 5, + renderElement: renderFeaturedExtensions, + rankElement: (extension) => { if (this.getHiddenCategories().has(extension.id)) { return null; } return 0; }, + contextService: this.contextService, + }); + + this.featuredExtensions?.then(extensions => { + this.telemetryService.publicLog2<{ featuredExtensions: string[]; title: string }, GettingStartedLayoutEventClassification>('gettingStarted.layout', { featuredExtensions: extensions.map(e => e.id), title: this.featuredExtensionService.title }); + featuredExtensionsList.setEntries(extensions); + }); + + this.featuredExtensionsList?.onDidChange(() => { + + this.registerDispatchListeners(); + }); + return featuredExtensionsList; + } + layout(size: Dimension) { this.detailsScrollbar?.scanDomNode(); @@ -1053,6 +1168,7 @@ export class GettingStartedPage extends EditorPane { this.startList?.layout(size); this.gettingStartedList?.layout(size); + this.featuredExtensionsList?.layout(size); this.recentlyOpenedList?.layout(size); this.layoutMarkdown?.(); @@ -1077,7 +1193,6 @@ export class GettingStartedPage extends EditorPane { const progress = (stats.stepsComplete / stats.stepsTotal) * 100; bar.style.width = `${progress}%`; - (element.parentElement as HTMLElement).classList.toggle('no-progress', stats.stepsComplete === 0); if (stats.stepsTotal === stats.stepsComplete) { @@ -1239,7 +1354,9 @@ export class GettingStartedPage extends EditorPane { this.detailsPageDisposables.clear(); const category = this.gettingStartedCategories.find(category => category.id === categoryID); - if (!category) { throw Error('could not find category with ID ' + categoryID); } + if (!category) { + throw Error('could not find category with ID ' + categoryID); + } const categoryDescriptorComponent = $('.getting-started-category', @@ -1275,6 +1392,8 @@ export class GettingStartedPage extends EditorPane { const contextKeysToWatch = new Set(category.steps.flatMap(step => step.when.keys())); const buildStepList = () => { + + category.steps.sort((a, b) => a.order - b.order); const toRender = category.steps .filter(step => this.contextService.contextMatchesRules(step.when)); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 6e7fdce8434708..1a6eaac0d7971e 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -18,7 +18,7 @@ import { joinPath } from 'vs/base/common/resources'; import { FileAccess } from 'vs/base/common/network'; import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ThemeIcon } from 'vs/base/common/themables'; -import { walkthroughs } from 'vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent'; +import { BuiltinGettingStartedCategory, BuiltinGettingStartedStep, walkthroughs } from 'vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent'; import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -34,6 +34,8 @@ import { checkGlobFileExists } from 'vs/workbench/services/extensions/common/wor import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { disposableTimeout } from 'vs/base/common/async'; export const HasMultipleNewFileEntries = new RawContextKey('hasMultipleNewFileEntries', false); @@ -114,6 +116,12 @@ export interface IWalkthroughsService { const DAYS = 24 * 60 * 60 * 1000; const NEW_WALKTHROUGH_TIME = 7 * DAYS; +type WalkthroughTreatment = { + walkthroughId: string; + walkthroughStepIds?: string[]; + stepOrder: number[]; +}; + export class WalkthroughsService extends Disposable implements IWalkthroughsService { declare readonly _serviceBrand: undefined; @@ -160,6 +168,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ @IViewsService private readonly viewsService: IViewsService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkbenchAssignmentService tasExperimentService: IWorkbenchAssignmentService, + @IProductService private readonly productService: IProductService, ) { super(); @@ -185,7 +194,30 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this.installedExtensionsRegistered = new Promise(r => this.triggerInstalledExtensionsRegistered = r); + this._register(disposableTimeout(() => this.registerBuiltInWalkthroughs().finally(/*do nothing*/), 0)); + } + + private async registerBuiltInWalkthroughs() { + + const treatmentString = await Promise.race([ + this.tasExperimentService?.getTreatment('welcome.walkthrough.content'), + new Promise(resolve => setTimeout(() => resolve(''), 2000)) + ]); + + const treatment: WalkthroughTreatment = treatmentString ? JSON.parse(treatmentString) : { walkthroughId: '' }; + walkthroughs.forEach(async (category, index) => { + let shouldReorder = false; + if (category.id === treatment?.walkthroughId) { + category = this.updateWalkthroughContent(category, treatment); + + shouldReorder = (treatment?.stepOrder !== undefined && category.content.steps.length === treatment.stepOrder.length); + if (shouldReorder) { + category.content.steps = category.content.steps.filter((_step, index) => treatment.stepOrder[index] >= 0); + treatment.stepOrder = treatment.stepOrder.filter(value => value >= 0); + } + } + this._registerWalkthrough({ ...category, icon: { type: 'icon', icon: category.icon }, @@ -199,7 +231,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ completionEvents: step.completionEvents ?? [], description: parseDescription(step.description), category: category.id, - order: index, + order: shouldReorder ? (treatment?.stepOrder ? treatment.stepOrder[index] : index) : index, when: ContextKeyExpr.deserialize(step.when) ?? ContextKeyExpr.true(), media: step.media.type === 'image' ? { @@ -225,6 +257,30 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ }); } + private updateWalkthroughContent(walkthrough: BuiltinGettingStartedCategory, experimentTreatment: WalkthroughTreatment): BuiltinGettingStartedCategory { + + if (!experimentTreatment?.walkthroughStepIds) { + return walkthrough; + } + + const walkthroughMetadata = this.productService.walkthroughMetadata?.find(value => value.id === walkthrough.id); + for (const step of experimentTreatment.walkthroughStepIds) { + const stepMetadata = walkthroughMetadata?.steps.find(value => value.id === step); + if (stepMetadata) { + + const newStep: BuiltinGettingStartedStep = { + id: step, + title: stepMetadata.title, + description: stepMetadata.description, + when: stepMetadata.when, + media: stepMetadata.media + }; + walkthrough.content.steps.push(newStep); + } + } + return walkthrough; + } + private initCompletionEventListeners() { this._register(this.commandService.onDidExecuteCommand(command => this.progressByEvent(`onCommand:${command.commandId}`))); @@ -654,7 +710,6 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ const parseDescription = (desc: string): LinkedText[] => desc.split('\n').filter(x => x).map(text => parseLinkedText(text)); - export const convertInternalMediaPathToFileURI = (path: string) => path.startsWith('https://') ? URI.parse(path, true) : FileAccess.asFileUri(`vs/workbench/contrib/welcomeGettingStarted/common/media/${path}`); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index de0df315e94179..f6b30a20d98791 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -142,7 +142,7 @@ display: none; } -.monaco-workbench .part.editor>.content .gettingStartedContainer.noWalkthroughs .gettingStartedSlideCategories li.showWalkthroughsEntry { +.monaco-workbench .part.editor>.content .gettingStartedContainer.noWalkthroughs .gettingStartedSlideCategories li.showWalkthroughsEntry, .gettingStartedContainer.noExtensions { display: unset; } @@ -273,7 +273,6 @@ line-height: normal; margin: 8px 8px 8px 0; padding: 3px 6px 6px; - left: -3px; text-align: left; } @@ -291,7 +290,7 @@ width: 100%; } -.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .description-content { +.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .description-content .featured-description-content { text-align: left; margin-left: 28px; } @@ -305,6 +304,15 @@ margin-bottom: 8px; } +.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .featured-description-content:not(:empty){ + margin-bottom: 8px; + margin-left: 48px; + overflow: hidden; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; +} + .monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .new-badge { justify-self: flex-end; align-self: flex-start; @@ -354,6 +362,14 @@ top: auto; } +.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideCategories .getting-started-category img.featured-icon { + padding-right: 8px; + max-width: 40px; + border-radius: 10%; + max-height: 40px; + position: relative; + top: auto; +} .monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-category img.category-icon { margin-right: 10px; margin-left: 10px; @@ -612,6 +628,14 @@ text-align: center; } +.monaco-workbench .part.editor>.content .gettingStartedContainer.noExtensions .index-list.featured-extensions { + display: none; +} + +.monaco-workbench .part.editor>.content .gettingStartedContainer.noWalkthroughs .index-list.getting-started { + display: none; +} + .monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-detail-right img { object-fit: contain; cursor: unset;