Adding link support to infobox. (#18876)

This commit is contained in:
Aasim Khan
2022-04-11 20:39:41 -07:00
committed by GitHub
parent 170950dca8
commit dd2d6e0b5c
12 changed files with 178 additions and 19 deletions

View File

@@ -1413,9 +1413,13 @@ declare module 'azdata' {
*/
export interface InfoBoxComponent extends Component, InfoBoxComponentProperties {
/**
* An event called when the InfoBox is clicked
* An event fired when the InfoBox is clicked
*/
onDidClick: vscode.Event<void>;
/**
* An event fired when the Infobox link is clicked
*/
onLinkClick: vscode.Event<InfoBoxLinkClickEventArgs>;
}
export interface InfoBoxComponentProperties {
@@ -1429,5 +1433,27 @@ declare module 'azdata' {
* Sets the ariaLabel for the right arrow button that shows up in clickable infoboxes
*/
clickableButtonAriaLabel?: string;
/**
* List of links to embed within the text. If links are specified there must be placeholder
* values in the value indicating where the links should be placed, in the format {i}
*
* e.g. "Click {0} for more information!""
*/
links?: LinkArea[];
}
/**
* Event argument for infobox link click event.
*/
export interface InfoBoxLinkClickEventArgs {
/**
* Index of the link selected
*/
index: number;
/**
* Link that is clicked
*/
link: LinkArea;
}
}

View File

@@ -22,7 +22,8 @@ export enum ComponentEventType {
onCellAction,
onEnterKeyPressed,
onInput,
onComponentLoaded
onComponentLoaded,
onChildClick
}
/**

View File

@@ -2085,7 +2085,8 @@ class InfoBoxComponentWrapper extends ComponentWrapper implements azdata.InfoBox
constructor(proxy: MainThreadModelViewShape, handle: number, id: string, logService: ILogService) {
super(proxy, handle, ModelComponentTypes.InfoBox, id, logService);
this.properties = {};
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<void>());
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<any>());
this._emitterMap.set(ComponentEventType.onChildClick, new Emitter<any>());
}
public get style(): azdata.InfoBoxStyle {
@@ -2104,6 +2105,14 @@ class InfoBoxComponentWrapper extends ComponentWrapper implements azdata.InfoBox
this.setProperty('text', v);
}
public get links(): azdata.LinkArea[] {
return this.properties['links'];
}
public set links(v: azdata.LinkArea[]) {
this.setProperty('links', v);
}
public get announceText(): boolean {
return this.properties['announceText'];
}
@@ -2128,10 +2137,15 @@ class InfoBoxComponentWrapper extends ComponentWrapper implements azdata.InfoBox
this.setProperty('clickableButtonAriaLabel', v);
}
public get onDidClick(): vscode.Event<any> {
public get onDidClick(): vscode.Event<void> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
public get onLinkClick(): vscode.Event<azdata.InfoBoxLinkClickEventArgs> {
let emitter = this._emitterMap.get(ComponentEventType.onChildClick);
return emitter && emitter.event;
}
}
class SliderComponentWrapper extends ComponentWrapper implements azdata.SliderComponent {

View File

@@ -246,7 +246,8 @@ export enum ComponentEventType {
onCellAction,
onEnterKeyPressed,
onInput,
onComponentLoaded
onComponentLoaded,
onChildClick
}
export interface IComponentEventArgs {

View File

@@ -11,9 +11,10 @@ import * as azdata from 'azdata';
import { ComponentEventType, IComponent, IComponentDescriptor, IModelStore } from 'sql/platform/dashboard/browser/interfaces';
import { ILogService } from 'vs/platform/log/common/log';
import { ComponentBase } from 'sql/workbench/browser/modelComponents/componentBase';
import { InfoBox, InfoBoxStyle } from 'sql/base/browser/ui/infoBox/infoBox';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { attachInfoBoxStyler } from 'sql/platform/theme/common/styler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { InfoBox, InfoBoxStyle } from 'sql/workbench/browser/ui/infoBox/infoBox';
@Component({
selector: 'modelview-infobox',
@@ -31,7 +32,8 @@ export default class InfoBoxComponent extends ComponentBase<azdata.InfoBoxCompon
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IWorkbenchThemeService) private _themeService: IWorkbenchThemeService,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(ILogService) logService: ILogService) {
super(changeRef, el, logService);
}
@@ -39,14 +41,20 @@ export default class InfoBoxComponent extends ComponentBase<azdata.InfoBoxCompon
ngAfterViewInit(): void {
this.baseInit();
if (this._container) {
this._infoBox = new InfoBox(this._container.nativeElement);
this._register(attachInfoBoxStyler(this._infoBox, this.themeService));
this._infoBox = this._instantiationService.createInstance(InfoBox, this._container.nativeElement, undefined);
this._register(attachInfoBoxStyler(this._infoBox, this._themeService));
this._infoBox.onDidClick(e => {
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: e
});
});
this._infoBox.onLinkClick(e => {
this.fireEvent({
eventType: ComponentEventType.onChildClick,
args: e
});
});
this.updateInfoBox();
}
}
@@ -70,6 +78,7 @@ export default class InfoBoxComponent extends ComponentBase<azdata.InfoBoxCompon
this._container.nativeElement.style.height = this.getHeight();
this._infoBox.announceText = this.announceText;
this._infoBox.infoBoxStyle = this.style;
this._infoBox.links = this.links;
this._infoBox.text = this.text;
this._infoBox.isClickable = this.isClickable;
this._infoBox.clickableButtonAriaLabel = this.clickableButtonAriaLabel;
@@ -95,4 +104,8 @@ export default class InfoBoxComponent extends ComponentBase<azdata.InfoBoxCompon
public get clickableButtonAriaLabel(): string {
return this.getPropertyOrDefault<string>((props) => props.clickableButtonAriaLabel, '');
}
public get links(): azdata.LinkArea[] {
return this.getPropertyOrDefault<azdata.LinkArea[]>((props) => props.links, []);
}
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/infoBox';
import * as azdata from 'azdata';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { alert, status } from 'vs/base/browser/ui/aria/aria';
import { IThemable } from 'vs/base/common/styler';
@@ -13,6 +14,8 @@ import { Event, Emitter } from 'vs/base/common/event';
import { Codicon } from 'vs/base/common/codicons';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ILogService } from 'vs/platform/log/common/log';
export interface IInfoBoxStyles {
informationBackground?: Color;
@@ -25,6 +28,7 @@ export type InfoBoxStyle = 'information' | 'warning' | 'error' | 'success';
export interface InfoBoxOptions {
text: string;
links?: azdata.LinkArea[];
style: InfoBoxStyle;
announceText?: boolean;
isClickable?: boolean;
@@ -37,6 +41,7 @@ export class InfoBox extends Disposable implements IThemable {
private _infoBoxElement: HTMLDivElement;
private _clickableIndicator: HTMLDivElement;
private _text = '';
private _links: azdata.LinkArea[] = [];
private _infoBoxStyle: InfoBoxStyle = 'information';
private _styles: IInfoBoxStyles;
private _announceText: boolean = false;
@@ -47,7 +52,16 @@ export class InfoBox extends Disposable implements IThemable {
private _onDidClick: Emitter<void> = this._register(new Emitter<void>());
get onDidClick(): Event<void> { return this._onDidClick.event; }
constructor(container: HTMLElement, options?: InfoBoxOptions) {
private _linkListenersDisposableStore = new DisposableStore();
private _onLinkClick: Emitter<azdata.InfoBoxLinkClickEventArgs> = this._register(new Emitter<azdata.InfoBoxLinkClickEventArgs>());
get onLinkClick(): Event<azdata.InfoBoxLinkClickEventArgs> { return this._onLinkClick.event; }
constructor(
container: HTMLElement,
options: InfoBoxOptions | undefined,
@IOpenerService private _openerService: IOpenerService,
@ILogService private _logService: ILogService
) {
super();
this._infoBoxElement = document.createElement('div');
this._imageElement = document.createElement('div');
@@ -63,6 +77,7 @@ export class InfoBox extends Disposable implements IThemable {
if (options) {
this.infoBoxStyle = options.style;
this.links = options.links;
this.text = options.text;
this._announceText = (options.announceText === true);
this.isClickable = (options.isClickable === true);
@@ -98,6 +113,15 @@ export class InfoBox extends Disposable implements IThemable {
this.updateStyle();
}
public get links(): azdata.LinkArea[] {
return this._links;
}
public set links(v: azdata.LinkArea[]) {
this._links = v ?? [];
this.createTextWithHyperlinks();
}
public get text(): string {
return this._text;
}
@@ -105,16 +129,96 @@ export class InfoBox extends Disposable implements IThemable {
public set text(text: string) {
if (this._text !== text) {
this._text = text;
this._textElement.innerText = text;
if (this.announceText) {
if (this.infoBoxStyle === 'warning' || this.infoBoxStyle === 'error') {
alert(text);
}
else {
status(text);
this.createTextWithHyperlinks();
}
}
public createTextWithHyperlinks() {
let text = this._text;
DOM.clearNode(this._textElement);
this._linkListenersDisposableStore.clear();
for (let i = 0; i < this._links.length; i++) {
const placeholderIndex = text.indexOf(`{${i}}`);
if (placeholderIndex < 0) {
this._logService.warn(`Could not find placeholder text {${i}} in text ${text}`);
// Just continue on so we at least show the rest of the text if just one was missed or something
continue;
}
// First insert any text from the start of the current string fragment up to the placeholder
let curText = text.slice(0, placeholderIndex);
if (curText) {
const span = DOM.$('span');
span.innerText = text.slice(0, placeholderIndex);
this._textElement.appendChild(span);
}
// Now insert the link element
const link = this._links[i];
/**
* If the url is empty, electron displays the link as visited.
* TODO: Investigate why it happens and fix the issue iin electron/vsbase.
*/
const linkElement = DOM.$('a', {
href: link.url === '' ? ' ' : link.url
});
linkElement.innerText = link.text;
if (link.accessibilityInformation) {
linkElement.setAttribute('aria-label', link.accessibilityInformation.label);
if (link.accessibilityInformation.role) {
linkElement.setAttribute('role', link.accessibilityInformation.role);
}
}
this._linkListenersDisposableStore.add(DOM.addDisposableListener(linkElement, DOM.EventType.CLICK, e => {
this._onLinkClick.fire({
index: i,
link: link
});
if (link.url) {
this.openLink(link.url);
}
e.stopPropagation();
}));
this._linkListenersDisposableStore.add(DOM.addDisposableListener(linkElement, DOM.EventType.KEY_PRESS, e => {
const event = new StandardKeyboardEvent(e);
if (this._isClickable && (event.equals(KeyCode.Enter) || !event.equals(KeyCode.Space))) {
this._onLinkClick.fire({
index: i,
link: link
});
if (link.url) {
this.openLink(link.url);
}
e.stopPropagation();
}
}));
this._textElement.appendChild(linkElement);
text = text.slice(placeholderIndex + 3);
}
if (text) {
const span = DOM.$('span');
span.innerText = text;
this._textElement.appendChild(span);
}
if (this.announceText) {
if (this.infoBoxStyle === 'warning' || this.infoBoxStyle === 'error') {
alert(text);
}
else {
status(text);
}
}
}
private openLink(href: string): void {
this._openerService.open(href);
}
public get isClickable(): boolean {

View File

Before

Width:  |  Height:  |  Size: 414 B

After

Width:  |  Height:  |  Size: 414 B

View File

Before

Width:  |  Height:  |  Size: 625 B

After

Width:  |  Height:  |  Size: 625 B

View File

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 911 B

View File

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -42,7 +42,7 @@ import { URI } from 'vs/base/common/uri';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
import { InfoBox } from 'sql/base/browser/ui/infoBox/infoBox';
import { InfoBox } from 'sql/workbench/browser/ui/infoBox/infoBox';
let azdataGraph = azdataGraphModule();
@@ -158,7 +158,7 @@ export class ExecutionPlanView implements IPanelView {
}
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingComplete', "Execution plans are generated");
} catch (e) {
this._loadingErrorInfoBox = new InfoBox(this._container, {
this._loadingErrorInfoBox = this.instantiationService.createInstance(InfoBox, this._container, {
text: e.toString(),
style: 'error',
isClickable: false