Improve getting started page
- Allow clicking checks to dismiss items - Allow hiding categories
This commit is contained in:
parent
e05ab31f37
commit
84fe402d65
|
@ -113,12 +113,22 @@ export interface IAuthenticationContribution {
|
|||
readonly label: string;
|
||||
}
|
||||
|
||||
export interface IGettingStartedContent {
|
||||
export interface IWelcomeItem {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly button: { title: string } & ({ command?: never, link: string } | { command: string, link?: never }),
|
||||
readonly media: { path: string | { hc: string, light: string, dark: string }, altText: string },
|
||||
readonly doneOn?:
|
||||
| { event: string; command?: never }
|
||||
| { event?: never; command: string };
|
||||
readonly when?: string;
|
||||
}
|
||||
|
||||
export interface IWelcomeCategory {
|
||||
readonly id: string,
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly when?: string;
|
||||
}
|
||||
|
||||
|
@ -141,7 +151,8 @@ export interface IExtensionContributions {
|
|||
readonly customEditors?: readonly IWebviewEditor[];
|
||||
readonly codeActions?: readonly ICodeActionContribution[];
|
||||
authentication?: IAuthenticationContribution[];
|
||||
gettingStarted?: IGettingStartedContent[];
|
||||
welcomeItems?: { [category: string]: IWelcomeItem[] };
|
||||
welcomeCategories?: IWelcomeCategory[];
|
||||
}
|
||||
|
||||
export type ExtensionKind = 'ui' | 'workspace' | 'web';
|
||||
|
|
|
@ -15,6 +15,8 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis
|
|||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
|
||||
|
||||
export * as icons from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedIcons';
|
||||
|
||||
|
@ -128,3 +130,18 @@ registerAction2(class extends Action2 {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
|
||||
.registerConfiguration({
|
||||
|
||||
...workbenchConfigurationNodeBase,
|
||||
'properties': {
|
||||
'gettingStarted.hiddenCategories': {
|
||||
'scope': ConfigurationScope.APPLICATION,
|
||||
'type': 'array',
|
||||
'items': { type: 'string' },
|
||||
'default': [],
|
||||
'description': localize('gettingStarted.hiddenCategories', "Hide categories of the welcome page's getting started section that are not relevant to you.")
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
width: 100%;
|
||||
user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer img {
|
||||
|
@ -183,6 +184,7 @@
|
|||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.categories .getting-started-category {
|
||||
padding-right: 46px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category .codicon {
|
||||
|
@ -191,8 +193,24 @@
|
|||
font-size: 20px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category img.category-icon {
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category .codicon.hide-category-button {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
font-size: 12px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.categories .getting-started-category img.category-icon {
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
padding-right: 8px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.detail .getting-started-category img.category-icon {
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
|
|
@ -39,10 +39,11 @@ import { splitName } from 'vs/base/common/labels';
|
|||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
|
||||
const SLIDE_TRANSITION_TIME_MS = 250;
|
||||
const configurationKey = 'workbench.startupEditor';
|
||||
|
||||
const hiddenEntriesConfigurationKey = 'gettingStarted.hiddenCategories';
|
||||
|
||||
export const gettingStartedInputTypeId = 'workbench.editors.gettingStartedInput';
|
||||
|
||||
export const inGettingStartedContext = new RawContextKey('inGettingStarted', false);
|
||||
|
@ -134,7 +135,13 @@ export class GettingStartedPage extends EditorPane {
|
|||
|
||||
super(GettingStartedPage.ID, telemetryService, themeService, storageService);
|
||||
|
||||
this.container = $('.gettingStartedContainer');
|
||||
this.container = $('.gettingStartedContainer',
|
||||
{
|
||||
role: 'document',
|
||||
tabindex: 0,
|
||||
'aria-label': localize('gettingStartedLabel', "Getting Started. Overview of how to get up to speed with your editor.")
|
||||
});
|
||||
|
||||
this.tasExperimentService = tasExperimentService;
|
||||
|
||||
this.contextService = this._register(contextService.createScoped(this.container));
|
||||
|
@ -224,9 +231,20 @@ export class GettingStartedPage extends EditorPane {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'hideCategory': {
|
||||
const selectedCategory = this.gettingStartedCategories.find(category => category.id === argument);
|
||||
if (!selectedCategory) { throw Error('Could not find category with ID ' + argument); }
|
||||
this.configurationService.updateValue(hiddenEntriesConfigurationKey,
|
||||
[...(this.configurationService.getValue<string[]>(hiddenEntriesConfigurationKey) ?? []), argument]);
|
||||
element.parentElement?.remove();
|
||||
break;
|
||||
}
|
||||
case 'selectTask': {
|
||||
this.selectTask(argument);
|
||||
e.stopPropagation();
|
||||
break;
|
||||
}
|
||||
case 'completeTask': {
|
||||
this.gettingStartedService.progressTask(argument);
|
||||
break;
|
||||
}
|
||||
case 'runTaskAction': {
|
||||
|
@ -242,7 +260,6 @@ export class GettingStartedPage extends EditorPane {
|
|||
} else {
|
||||
throw Error('Task ' + JSON.stringify(taskToRun) + ' does not have an associated action');
|
||||
}
|
||||
e.stopPropagation();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
@ -250,6 +267,7 @@ export class GettingStartedPage extends EditorPane {
|
|||
break;
|
||||
}
|
||||
}
|
||||
e.stopPropagation();
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
@ -331,9 +349,7 @@ export class GettingStartedPage extends EditorPane {
|
|||
);
|
||||
|
||||
const gettingStartedPage =
|
||||
$('.gettingStarted.welcomePageFocusElement', {
|
||||
role: 'document',
|
||||
'aria-label': localize('gettingStartedLabel', "Getting Started. Overview of how to get up to speed with your editor.")
|
||||
$('.gettingStarted', {
|
||||
},
|
||||
$('.gettingStartedSlideCategory.gettingStartedSlide.categories'),
|
||||
tasksSlide
|
||||
|
@ -350,30 +366,39 @@ export class GettingStartedPage extends EditorPane {
|
|||
}
|
||||
|
||||
private async buildCategoriesSlide() {
|
||||
const categoryElements = this.gettingStartedCategories.filter(entry => entry.content.type === 'items').map(
|
||||
category => {
|
||||
const categoryDescriptionElement =
|
||||
category.content.type === 'items' ?
|
||||
$('.category-description-container', {},
|
||||
$('h3.category-title', {}, category.title),
|
||||
// $('.category-description.description', { 'aria-label': category.description + ' ' + localize('pressEnterToSelect', "Press Enter to Select") }, category.description),
|
||||
$('.category-progress', { 'x-data-category-id': category.id, },
|
||||
// $('.message'),
|
||||
$('.progress-bar-outer', { 'role': 'progressbar' },
|
||||
$('.progress-bar-inner'))))
|
||||
:
|
||||
$('.category-description-container', {},
|
||||
$('h3.category-title', {}, category.title),
|
||||
$('.category-description.description', { 'aria-label': category.description + ' ' + localize('pressEnterToSelect', "Press Enter to Select") }, category.description));
|
||||
const hiddenCategories = new Set(this.configurationService.getValue(hiddenEntriesConfigurationKey) ?? []);
|
||||
const categoryElements = this.gettingStartedCategories
|
||||
.filter(entry => entry.content.type === 'items')
|
||||
.filter(entry => !hiddenCategories.has(entry.id))
|
||||
.map(
|
||||
category => {
|
||||
const categoryDescriptionElement =
|
||||
category.content.type === 'items' ?
|
||||
$('.category-description-container', {},
|
||||
$('h3.category-title', {}, category.title),
|
||||
// $('.category-description.description', { 'aria-label': category.description + ' ' + localize('pressEnterToSelect', "Press Enter to Select") }, category.description),
|
||||
$('.category-progress', { 'x-data-category-id': category.id, },
|
||||
// $('.message'),
|
||||
$('.progress-bar-outer', { 'role': 'progressbar' },
|
||||
$('.progress-bar-inner'))))
|
||||
:
|
||||
$('.category-description-container', {},
|
||||
$('h3.category-title', {}, category.title),
|
||||
$('.category-description.description', { 'aria-label': category.description + ' ' + localize('pressEnterToSelect', "Press Enter to Select") }, category.description));
|
||||
|
||||
return $('button.getting-started-category',
|
||||
{
|
||||
'x-dispatch': 'selectCategory:' + category.id,
|
||||
'role': 'listitem',
|
||||
},
|
||||
this.iconWidgetFor(category),
|
||||
categoryDescriptionElement);
|
||||
});
|
||||
return $('button.getting-started-category',
|
||||
{
|
||||
'x-dispatch': 'selectCategory:' + category.id,
|
||||
'role': 'listitem',
|
||||
'title': category.description
|
||||
},
|
||||
this.iconWidgetFor(category),
|
||||
$('a.codicon.codicon-close.hide-category-button', {
|
||||
'x-dispatch': 'hideCategory:' + category.id,
|
||||
'title': localize('close', "Hide"),
|
||||
}),
|
||||
categoryDescriptionElement);
|
||||
});
|
||||
|
||||
const categoryScrollContainer = $('.getting-started-categories-scrolling-container');
|
||||
const categoriesContainer = $('ul.getting-started-categories-container', { 'role': 'list' });
|
||||
|
@ -542,8 +567,8 @@ export class GettingStartedPage extends EditorPane {
|
|||
|
||||
if (entry.content.type === 'items') { return undefined; }
|
||||
|
||||
const li = $('li', { 'x-dispatch': 'selectCategory:' + entry.id }, this.iconWidgetFor(entry));
|
||||
const button = $<HTMLAnchorElement>('button.button-link');
|
||||
const li = $('li', {}, this.iconWidgetFor(entry));
|
||||
const button = $<HTMLAnchorElement>('button.button-link', { 'x-dispatch': 'selectCategory:' + entry.id });
|
||||
|
||||
button.innerText = entry.title;
|
||||
button.title = entry.description;
|
||||
|
@ -597,8 +622,8 @@ export class GettingStartedPage extends EditorPane {
|
|||
bar.setAttribute('aria-valuemin', '0');
|
||||
bar.setAttribute('aria-valuenow', '' + numDone);
|
||||
bar.setAttribute('aria-valuemax', '' + numTotal);
|
||||
|
||||
bar.style.width = `${(numDone / numTotal) * 100}%`;
|
||||
const progress = Math.max((numDone / numTotal) * 100, 3);
|
||||
bar.style.width = `${progress}%`;
|
||||
|
||||
if (numTotal === numDone) {
|
||||
bar.title = `All items complete!`;
|
||||
|
@ -657,7 +682,11 @@ export class GettingStartedPage extends EditorPane {
|
|||
'aria-checked': '' + task.done,
|
||||
'role': 'listitem',
|
||||
},
|
||||
$('.codicon' + (task.done ? '.complete' + ThemeIcon.asCSSSelector(gettingStartedCheckedCodicon) : ThemeIcon.asCSSSelector(gettingStartedUncheckedCodicon)), { 'data-done-task-id': task.id }),
|
||||
$('.codicon' + (task.done ? '.complete' + ThemeIcon.asCSSSelector(gettingStartedCheckedCodicon) : ThemeIcon.asCSSSelector(gettingStartedUncheckedCodicon)),
|
||||
{
|
||||
'data-done-task-id': task.id,
|
||||
'x-dispatch': 'completeTask:' + task.id,
|
||||
}),
|
||||
$('.task-description-container', {},
|
||||
$('h3.task-title', {}, task.title),
|
||||
$('.task-description.description', {}, task.description),
|
||||
|
@ -751,15 +780,6 @@ export class GettingStartedPage extends EditorPane {
|
|||
}
|
||||
}
|
||||
|
||||
private focusFirstUncompletedCategory() {
|
||||
let toFocus!: HTMLElement;
|
||||
this.container.querySelectorAll('.category-progress').forEach(progress => {
|
||||
const progressAmount = assertIsDefined(progress.querySelector('.progress-bar-inner') as HTMLDivElement).style.width;
|
||||
if (!toFocus && progressAmount !== '100%') { toFocus = assertIsDefined(progress.parentElement?.parentElement); }
|
||||
});
|
||||
(toFocus ?? assertIsDefined(this.container.querySelector('button.getting-started-category')) as HTMLButtonElement)?.focus();
|
||||
}
|
||||
|
||||
private setSlide(toEnable: 'details' | 'categories') {
|
||||
const slideManager = assertIsDefined(this.container.querySelector('.gettingStarted'));
|
||||
if (toEnable === 'categories') {
|
||||
|
@ -767,7 +787,7 @@ export class GettingStartedPage extends EditorPane {
|
|||
slideManager.classList.add('showCategories');
|
||||
this.container.querySelector('.gettingStartedSlideDetails')!.querySelectorAll('button').forEach(button => button.disabled = true);
|
||||
this.container.querySelector('.gettingStartedSlideCategory')!.querySelectorAll('button').forEach(button => button.disabled = false);
|
||||
this.focusFirstUncompletedCategory();
|
||||
this.container.focus();
|
||||
} else {
|
||||
slideManager.classList.add('showDetails');
|
||||
slideManager.classList.remove('showCategories');
|
||||
|
|
|
@ -174,97 +174,157 @@ Registry.add(GettingStartedRegistryID, registryImpl);
|
|||
export const GettingStartedRegistry: IGettingStartedRegistry = Registry.as(GettingStartedRegistryID);
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint({
|
||||
extensionPoint: 'gettingStarted',
|
||||
extensionPoint: 'welcomeItems',
|
||||
jsonSchema: {
|
||||
doNotSuggest: true,
|
||||
description: localize('gettingStarted', "Contribute items to help users in getting started with your extension. Rendering and progression through these items is managed by core. Experimental, requires proposedApi."),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['id', 'title', 'description', 'button', 'media'],
|
||||
defaultSnippets: [{ body: { 'id': '$1', 'title': '$2', 'description': '$3', 'button': { 'title': '$4' }, 'media': { 'path': '$5', 'altText': '$6' } } }],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.id', "Unique identifier for this item."),
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.title', "Title of item.")
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.description', "Description of item.")
|
||||
},
|
||||
button: {
|
||||
description: localize('gettingStarted.button', "The item's button, which can either link to an external resource or run a command"),
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
required: ['title', 'command'],
|
||||
defaultSnippets: [{ 'body': { 'title': '$1', 'command': '$2' } }],
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.button.title', "Title of button.")
|
||||
},
|
||||
command: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.button.command', "Command to run when button is clicked. Running this command will mark the item completed.")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
required: ['title', 'link'],
|
||||
defaultSnippets: [{ 'body': { 'title': '$1', 'link': '$2' } }],
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.button.title', "Title of button.")
|
||||
},
|
||||
link: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.button.link', "Link to open when button is clicked. Opening this link will mark the item completed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
media: {
|
||||
type: 'object',
|
||||
required: ['path', 'altText'],
|
||||
description: localize('gettingStarted.media', "Image to show alongside this item."),
|
||||
defaultSnippets: [{ 'body': { 'altText': '$1' } }],
|
||||
properties: {
|
||||
path: {
|
||||
description: localize('gettingStarted.media.path', "Either a single string path to an image to be used on all color themes, or separate paths for light, dark, and high contrast themes."),
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
defaultSnippets: [{ 'body': '$1' }],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
defaultSnippets: [{ 'body': { 'hc': '$1', 'light': '$2', 'dark': '$3' } }],
|
||||
required: ['hc', 'light', 'dark'],
|
||||
properties: {
|
||||
hc: { type: 'string' },
|
||||
light: { type: 'string' },
|
||||
dark: { type: 'string' },
|
||||
description: localize('gettingStarted', "Contribute items to help users in getting started with your extension. Keys correspond to categories contributed via welcomeCategories contribution point. Experimental, available in VS Code Insiders only."),
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['id', 'title', 'description', 'button', 'media'],
|
||||
defaultSnippets: [{ body: { 'id': '$1', 'title': '$2', 'description': '$3', 'button': { 'title': '$4' }, 'media': { 'path': '$5', 'altText': '$6' } } }],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.id', "Unique identifier for this item."),
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.title', "Title of item.")
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.description', "Description of item.")
|
||||
},
|
||||
button: {
|
||||
description: localize('gettingStarted.button', "The item's button, which can either link to an external resource or run a command"),
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
required: ['title', 'command'],
|
||||
defaultSnippets: [{ 'body': { 'title': '$1', 'command': '$2' } }],
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.button.title', "Title of button.")
|
||||
},
|
||||
command: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.button.command', "Command to run when button is clicked. Running this command will mark the item completed.")
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
altText: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.media.altText', "Alternate text to display when the image cannot be loaded or in screen readers.")
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
required: ['title', 'link'],
|
||||
defaultSnippets: [{ 'body': { 'title': '$1', 'link': '$2' } }],
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.button.title', "Title of button.")
|
||||
},
|
||||
link: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.button.link', "Link to open when button is clicked. Opening this link will mark the item completed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
media: {
|
||||
type: 'object',
|
||||
required: ['path', 'altText'],
|
||||
description: localize('gettingStarted.media', "Image to show alongside this item."),
|
||||
defaultSnippets: [{ 'body': { 'altText': '$1' } }],
|
||||
properties: {
|
||||
path: {
|
||||
description: localize('gettingStarted.media.path', "Either a single string path to an image to be used on all color themes, or separate paths for light, dark, and high contrast themes."),
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
defaultSnippets: [{ 'body': '$1' }],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
defaultSnippets: [{ 'body': { 'hc': '$1', 'light': '$2', 'dark': '$3' } }],
|
||||
required: ['hc', 'light', 'dark'],
|
||||
properties: {
|
||||
hc: { type: 'string' },
|
||||
light: { type: 'string' },
|
||||
dark: { type: 'string' },
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
altText: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.media.altText', "Alternate text to display when the image cannot be loaded or in screen readers.")
|
||||
}
|
||||
}
|
||||
},
|
||||
doneOn: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
required: ['event'],
|
||||
properties: {
|
||||
'event': {
|
||||
description: localize('gettingStarted.oneOn.event', "Mark item done when the specified event is marked via the invoking the `welcomeItems.markEvent` command."),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
required: ['command'],
|
||||
properties: {
|
||||
'command': {
|
||||
description: localize('gettingStarted.oneOn.command', "Mark item done when the specified command is executed."),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
description: localize('gettingStarted.doneOn', "Signal to mark item as complete. If not defined, running the button's command will mark the item complete.")
|
||||
},
|
||||
when: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.when', "Context key expression to control the visibility of this getting started item.")
|
||||
}
|
||||
},
|
||||
when: {
|
||||
type: 'string',
|
||||
description: localize('gettingStarted.when', "Context key expression to control the visibility of this getting started item.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint({
|
||||
extensionPoint: 'welcomeCategories',
|
||||
jsonSchema: {
|
||||
doNotSuggest: true,
|
||||
description: localize('welcomeCategories', "Contribute categories of items to help users in getting started with your extension. Items themselves are contributed via welcomeItems contribution point. Experimental, available in VS Code Insiders only."),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['id', 'title', 'description'],
|
||||
defaultSnippets: [{ body: { 'id': '$1', 'title': '$2', 'description': '$3' } }],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: localize('welcomeCategories.id', "Unique identifier for this category."),
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: localize('welcomeCategories.title', "Title of category.")
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: localize('welcomeCategories.description', "Description of category.")
|
||||
},
|
||||
when: {
|
||||
type: 'string',
|
||||
description: localize('welcomeCategories.when', "Context key expression to control the visibility of this category.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio
|
|||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { localize } from 'vs/nls';
|
||||
import { DefaultIconPath } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
|
||||
export const IGettingStartedService = createDecorator<IGettingStartedService>('gettingStartedService');
|
||||
|
||||
|
@ -50,6 +50,7 @@ export interface IGettingStartedService {
|
|||
getCategories(): IGettingStartedCategoryWithProgress[]
|
||||
|
||||
progressByEvent(eventName: string): void;
|
||||
progressTask(id: string): void;
|
||||
}
|
||||
|
||||
export class GettingStartedService extends Disposable implements IGettingStartedService {
|
||||
|
@ -137,18 +138,20 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
|||
if (!this.trackedExtensions.has(ExtensionIdentifier.toKey(extension.identifier))) {
|
||||
this.trackedExtensions.add(ExtensionIdentifier.toKey(extension.identifier));
|
||||
|
||||
if (extension.contributes?.gettingStarted?.length) {
|
||||
if (!extension.enableProposedApi) {
|
||||
console.warn('Extension', extension.identifier.value, 'contributes getting started content but has not enabled proposedApi. The contributed content will be disregarded.');
|
||||
return;
|
||||
}
|
||||
if ((extension.contributes?.welcomeCategories || extension.contributes?.welcomeItems) && product.quality === 'stable') {
|
||||
console.warn('Extension', extension.identifier.value, 'contributes welcome page content but this is a Stable build and extension contributions are only available in Insiders. The contributed content will be disregarded.');
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryID = `EXTContrib-${extension.identifier.value}`;
|
||||
const contributedCategories = new Map();
|
||||
|
||||
extension.contributes?.welcomeCategories?.forEach(category => {
|
||||
const categoryID = extension.identifier.value + '.' + category.id;
|
||||
contributedCategories.set(category.id, categoryID);
|
||||
this.registry.registerCategory({
|
||||
content: { type: 'items' },
|
||||
description: localize('extContrib', "Learn more about {0}!", extension.displayName ?? extension.name),
|
||||
title: extension.displayName || extension.name,
|
||||
description: category.description,
|
||||
title: category.title,
|
||||
id: categoryID,
|
||||
icon: {
|
||||
type: 'image',
|
||||
|
@ -156,22 +159,31 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
|||
? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true)
|
||||
: DefaultIconPath
|
||||
},
|
||||
when: ContextKeyExpr.true(),
|
||||
when: ContextKeyExpr.deserialize(category.when) ?? ContextKeyExpr.true(),
|
||||
});
|
||||
extension.contributes?.gettingStarted.forEach((content, index) => {
|
||||
});
|
||||
|
||||
Object.entries(extension.contributes?.welcomeItems ?? {}).forEach(([category, items]) =>
|
||||
items.forEach((item, index) =>
|
||||
this.registry.registerTask({
|
||||
button: content.button,
|
||||
description: content.description,
|
||||
media: { type: 'image', altText: content.media.altText, path: convertPaths(content.media.path) },
|
||||
doneOn: content.button.command ? { commandExecuted: content.button.command } : { eventFired: `linkOpened:${content.button.link}` },
|
||||
id: content.id,
|
||||
title: content.title,
|
||||
when: ContextKeyExpr.deserialize(content.when) ?? ContextKeyExpr.true(),
|
||||
category: categoryID,
|
||||
button: item.button,
|
||||
description: item.description,
|
||||
media: { type: 'image', altText: item.media.altText, path: convertPaths(item.media.path) },
|
||||
doneOn: item.doneOn?.event
|
||||
? { eventFired: item.doneOn.event }
|
||||
: item.doneOn?.command ?
|
||||
{ commandExecuted: item.doneOn.command }
|
||||
: item.button.command
|
||||
? { commandExecuted: item.button.command }
|
||||
: { eventFired: `linkOpened:${item.button.link}` },
|
||||
id: extension.identifier.value + '.' + item.id,
|
||||
title: item.title,
|
||||
when: ContextKeyExpr.deserialize(item.when) ?? ContextKeyExpr.true(),
|
||||
category: contributedCategories.get(category) ?? category,
|
||||
order: index,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,7 +253,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
|||
};
|
||||
}
|
||||
|
||||
private progressTask(id: string) {
|
||||
progressTask(id: string) {
|
||||
const oldProgress = this.taskProgress[id];
|
||||
if (!oldProgress || oldProgress.done !== true) {
|
||||
this.taskProgress[id] = { done: true };
|
||||
|
|
Loading…
Reference in a new issue