Adding wizard and dialog footer loading spinner (#21230)

* Adding wizard and dialog loading

* Moving apis to proposed

* fixing namespace

* Only firing event when the value changes

* Only firing when value is changed

* Adding loading complete message to dialog and wizard

* Registering listeners and making a new base interface for loading components

* Fixing api comment

* Renaming prop to loadingCompleted

* old loading icon
This commit is contained in:
Aasim Khan
2022-11-30 12:18:34 -08:00
committed by GitHub
parent 62d5c1f2d6
commit c0a194df4a
12 changed files with 242 additions and 14 deletions

View File

@@ -306,7 +306,10 @@ export function createViewContext(): ViewTestContext {
onClosed: new vscode.EventEmitter<azdata.window.CloseReason>().event, onClosed: new vscode.EventEmitter<azdata.window.CloseReason>().event,
registerContent: () => { }, registerContent: () => { },
modelView: undefined!, modelView: undefined!,
valid: true valid: true,
loading: false,
loadingText: '',
loadingCompletedText: ''
}; };
let wizard: azdata.window.Wizard = { let wizard: azdata.window.Wizard = {
title: '', title: '',
@@ -327,7 +330,10 @@ export function createViewContext(): ViewTestContext {
close: () => { return Promise.resolve(); }, close: () => { return Promise.resolve(); },
registerNavigationValidator: () => { }, registerNavigationValidator: () => { },
message: dialogMessage, message: dialogMessage,
registerOperation: () => { } registerOperation: () => { },
loading: false,
loadingText: '',
loadingCompletedText: ''
}; };
let wizardPage: azdata.window.WizardPage = { let wizardPage: azdata.window.WizardPage = {
title: '', title: '',

View File

@@ -134,6 +134,24 @@ declare module 'azdata' {
} }
} }
export interface LoadingComponentBase {
/**
* When true, the component will display a loading spinner.
*/
loading?: boolean;
/**
* This sets the alert text which gets announced when the loading spinner is shown.
*/
loadingText?: string;
/**
* The text to display while loading is set to false. Will also be announced through screen readers
* once loading is completed.
*/
loadingCompletedText?: string;
}
/** /**
* The column information of a data set. * The column information of a data set.
*/ */
@@ -1702,4 +1720,12 @@ declare module 'azdata' {
*/ */
objectType?: string; objectType?: string;
} }
export namespace window {
export interface Wizard extends LoadingComponentBase {
}
export interface Dialog extends LoadingComponentBase {
}
}
} }

View File

@@ -4,7 +4,6 @@
circle { circle {
animation: ball 0.6s linear infinite; animation: ball 0.6s linear infinite;
} }
circle:nth-child(2) { animation-delay: 0.075s; } circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; } circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; } circle:nth-child(4) { animation-delay: 0.225s; }
@@ -12,7 +11,6 @@
circle:nth-child(6) { animation-delay: 0.375s; } circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; } circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; } circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball { @keyframes ball {
0% { opacity: 1; } 0% { opacity: 1; }
100% { opacity: 0.3; } 100% { opacity: 0.3; }

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -144,6 +144,9 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM
dialog.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle)); dialog.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle));
} }
dialog.message = details.message; dialog.message = details.message;
dialog.loading = details.loading;
dialog.loadingText = dialog.loadingText;
dialog.loadingCompletedText = dialog.loadingCompletedText;
return Promise.resolve(); return Promise.resolve();
} }
@@ -223,7 +226,9 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM
wizard.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle)); wizard.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle));
} }
wizard.message = details.message; wizard.message = details.message;
wizard.loading = details.loading;
wizard.loadingText = details.loadingText;
wizard.loadingCompletedText = details.loadingCompletedText;
return Promise.resolve(); return Promise.resolve();
} }

View File

@@ -133,6 +133,9 @@ class DialogImpl extends ModelViewPanelImpl implements azdata.window.Dialog {
private _renderHeader: boolean; private _renderHeader: boolean;
private _renderFooter: boolean; private _renderFooter: boolean;
private _dialogProperties: IDialogProperties; private _dialogProperties: IDialogProperties;
private _loading: boolean;
private _loadingText: string;
private _loadingCompletedText: string;
private _onClosed = new Emitter<azdata.window.CloseReason>(); private _onClosed = new Emitter<azdata.window.CloseReason>();
public onClosed = this._onClosed.event; public onClosed = this._onClosed.event;
@@ -216,6 +219,33 @@ class DialogImpl extends ModelViewPanelImpl implements azdata.window.Dialog {
this._extHostModelViewDialog.updateDialogContent(this); this._extHostModelViewDialog.updateDialogContent(this);
} }
public get loading(): boolean {
return this._loading;
}
public set loading(value: boolean) {
this._loading = value;
this._extHostModelViewDialog.updateDialogContent(this);
}
public get loadingText(): string {
return this._loadingText;
}
public set loadingText(value: string) {
this._loadingText = value;
this._extHostModelViewDialog.updateDialogContent(this);
}
public get loadingCompletedText(): string {
return this._loadingCompletedText;
}
public set loadingCompletedText(value: string) {
this._loadingCompletedText = value;
this._extHostModelViewDialog.updateDialogContent(this);
}
public get dialogName(): string { public get dialogName(): string {
return this._dialogName; return this._dialogName;
} }
@@ -443,6 +473,9 @@ class WizardImpl implements azdata.window.Wizard {
public readonly onPageChanged = this._pageChangedEmitter.event; public readonly onPageChanged = this._pageChangedEmitter.event;
private _navigationValidator: (info: azdata.window.WizardPageChangeInfo) => boolean | Thenable<boolean>; private _navigationValidator: (info: azdata.window.WizardPageChangeInfo) => boolean | Thenable<boolean>;
private _message: azdata.window.DialogMessage; private _message: azdata.window.DialogMessage;
private _loading: boolean;
private _loadingText: string;
private _loadingCompletedText: string;
private _displayPageTitles: boolean = true; private _displayPageTitles: boolean = true;
private _operationHandler: BackgroundOperationHandler; private _operationHandler: BackgroundOperationHandler;
private _width: DialogWidth; private _width: DialogWidth;
@@ -487,6 +520,33 @@ class WizardImpl implements azdata.window.Wizard {
this._extHostModelViewDialog.updateWizard(this); this._extHostModelViewDialog.updateWizard(this);
} }
public get loading(): boolean {
return this._loading;
}
public set loading(value: boolean) {
this._loading = value;
this._extHostModelViewDialog.updateWizard(this);
}
public get loadingText(): string {
return this._loadingText
}
public set loadingText(value: string) {
this._loadingText = value;
this._extHostModelViewDialog.updateWizard(this);
}
public get loadingCompletedText(): string {
return this._loadingCompletedText;
}
public set loadingCompletedText(value: string) {
this._loadingCompletedText = value;
this._extHostModelViewDialog.updateWizard(this);
}
public get displayPageTitles(): boolean { public get displayPageTitles(): boolean {
return this._displayPageTitles; return this._displayPageTitles;
} }
@@ -797,7 +857,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
cancelButton: this.getHandle(dialog.cancelButton), cancelButton: this.getHandle(dialog.cancelButton),
content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getHandle(tab)) : dialog.content as string, content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getHandle(tab)) : dialog.content as string,
customButtons: dialog.customButtons ? dialog.customButtons.map(button => this.getHandle(button)) : undefined, customButtons: dialog.customButtons ? dialog.customButtons.map(button => this.getHandle(button)) : undefined,
message: dialog.message message: dialog.message,
loading: dialog.loading,
loadingText: dialog.loadingText,
loadingCompletedText: dialog.loadingCompletedText
}); });
} }
@@ -944,7 +1007,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
nextButton: this.getHandle(wizard.nextButton), nextButton: this.getHandle(wizard.nextButton),
customButtons: wizard.customButtons ? wizard.customButtons.map(button => this.getHandle(button)) : undefined, customButtons: wizard.customButtons ? wizard.customButtons.map(button => this.getHandle(button)) : undefined,
message: wizard.message, message: wizard.message,
displayPageTitles: wizard.displayPageTitles displayPageTitles: wizard.displayPageTitles,
loading: wizard.loading,
loadingText: wizard.loadingText,
loadingCompletedText: wizard.loadingCompletedText,
}); });
} }

View File

@@ -268,6 +268,9 @@ export interface IModelViewDialogDetails {
renderHeader: boolean; renderHeader: boolean;
renderFooter: boolean; renderFooter: boolean;
dialogProperties: IDialogProperties; dialogProperties: IDialogProperties;
loading: boolean;
loadingText: string;
loadingCompletedText: string;
} }
export interface IModelViewTabDetails { export interface IModelViewTabDetails {
@@ -307,6 +310,9 @@ export interface IModelViewWizardDetails {
message: DialogMessage; message: DialogMessage;
displayPageTitles: boolean; displayPageTitles: boolean;
width: DialogWidth; width: DialogWidth;
loading: boolean;
loadingText: string;
loadingCompletedText: string;
} }
export type DialogWidth = 'narrow' | 'medium' | 'wide' | number | string; export type DialogWidth = 'narrow' | 'medium' | 'wide' | number | string;

View File

@@ -81,6 +81,7 @@ export interface IModalOptions {
hasErrors?: boolean; hasErrors?: boolean;
hasSpinner?: boolean; hasSpinner?: boolean;
spinnerTitle?: string; spinnerTitle?: string;
onSpinnerHideText?: string;
renderHeader?: boolean; renderHeader?: boolean;
renderFooter?: boolean; renderFooter?: boolean;
dialogProperties?: IDialogProperties; dialogProperties?: IDialogProperties;
@@ -94,7 +95,7 @@ const defaultOptions: IModalOptions = {
hasBackButton: false, hasBackButton: false,
hasTitleIcon: false, hasTitleIcon: false,
hasErrors: false, hasErrors: false,
hasSpinner: false, hasSpinner: true,
renderHeader: true, renderHeader: true,
renderFooter: true, renderFooter: true,
dialogProperties: undefined dialogProperties: undefined
@@ -638,6 +639,9 @@ export abstract class Modal extends Disposable implements IThemable {
} }
} else { } else {
DOM.hide(this._spinnerElement!); DOM.hide(this._spinnerElement!);
if (this._modalOptions.onSpinnerHideText) {
alert(this._modalOptions.onSpinnerHideText);
}
} }
} }
} }

View File

@@ -9,8 +9,8 @@ import { Dialog, Wizard } from 'sql/workbench/services/dialog/common/dialogTypes
import { IModalOptions } from 'sql/workbench/browser/modal/modal'; import { IModalOptions } from 'sql/workbench/browser/modal/modal';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export const DefaultDialogOptions: IModalOptions = { hasBackButton: false, width: 'narrow', hasErrors: true }; export const DefaultDialogOptions: IModalOptions = { hasBackButton: false, width: 'narrow', hasErrors: true, hasSpinner: true };
export const DefaultWizardOptions: IModalOptions = { hasBackButton: false, width: 'wide', hasErrors: true }; export const DefaultWizardOptions: IModalOptions = { hasBackButton: false, width: 'wide', hasErrors: true, hasSpinner: true };
export class CustomDialogService { export class CustomDialogService {
private _dialogModals = new Map<Dialog, DialogModal>(); private _dialogModals = new Map<Dialog, DialogModal>();

View File

@@ -93,7 +93,17 @@ export class DialogModal extends Modal {
}; };
messageChangeHandler(this._dialog.message); messageChangeHandler(this._dialog.message);
this._dialog.onMessageChange(message => messageChangeHandler(message)); this._register(this._dialog.onMessageChange(message => messageChangeHandler(message)));
this._register(this._dialog.onLoadingChange((loadingState) => {
this.spinner = loadingState;
}));
this._register(this._dialog.onLoadingTextChange((loadingText) => {
this._modalOptions.spinnerTitle = loadingText;
}));
this._register(this._dialog.onLoadingCompletedTextChange((loadingCompletedText) => {
this._modalOptions.onSpinnerHideText = loadingCompletedText;
}));
} }
private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requireDialogValid: boolean = false): Button { private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requireDialogValid: boolean = false): Button {

View File

@@ -96,7 +96,21 @@ export class WizardModal extends Modal {
}; };
messageChangeHandler(this._wizard.message); messageChangeHandler(this._wizard.message);
this._wizard.onMessageChange(message => messageChangeHandler(message)); this._register(this._wizard.onMessageChange(message => messageChangeHandler(message)));
this._register(this._wizard.onLoadingChange((loadingState) => {
this.spinner = loadingState;
}));
this._register(this._wizard.onLoadingChange((loadingState) => {
this.spinner = loadingState;
}));
this._register(this._wizard.onLoadingTextChange((loadingText) => {
this._modalOptions.spinnerTitle = loadingText;
}));
this._register(this._wizard.onLoadingCompletedTextChange((loadingCompletedText) => {
this._modalOptions.onSpinnerHideText = loadingCompletedText;
}));
} }
private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requirePageValid: boolean = false, index?: number): Button { private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requirePageValid: boolean = false, index?: number): Button {

View File

@@ -53,6 +53,16 @@ export class Dialog extends ModelViewPane {
public customButtons: DialogButton[] = []; public customButtons: DialogButton[] = [];
private _onMessageChange = new Emitter<DialogMessage | undefined>(); private _onMessageChange = new Emitter<DialogMessage | undefined>();
public readonly onMessageChange = this._onMessageChange.event; public readonly onMessageChange = this._onMessageChange.event;
private _loading: boolean = false;
private _loadingText: string;
private _loadingCompletedText: string;
private _onLoadingChange = new Emitter<boolean | undefined>();
private _onLoadingTextChange = new Emitter<string | undefined>();
private _onLoadingCompletedTextChange = new Emitter<string | undefined>();
public readonly onLoadingChange = this._onLoadingChange.event;
public readonly onLoadingTextChange = this._onLoadingTextChange.event;
public readonly onLoadingCompletedTextChange = this._onLoadingCompletedTextChange.event;
private _message: DialogMessage | undefined; private _message: DialogMessage | undefined;
private _closeValidator: CloseValidator | undefined; private _closeValidator: CloseValidator | undefined;
@@ -87,6 +97,39 @@ export class Dialog extends ModelViewPane {
this._onMessageChange.fire(this._message); this._onMessageChange.fire(this._message);
} }
public get loading(): boolean {
return this._loading;
}
public set loading(value: boolean) {
if (this.loading !== value) {
this._loading = value;
this._onLoadingChange.fire(this._loading);
}
}
public get loadingText(): string | undefined {
return this._loadingText;
}
public set loadingText(value: string | undefined) {
if (this.loadingText !== value) {
this._loadingText = value;
this._onLoadingTextChange.fire(this._loadingText);
}
}
public get loadingCompletedText(): string | undefined {
return this._loadingCompletedText;
}
public set loadingCompletedText(value: string | undefined) {
if (this._loadingCompletedText !== value) {
this._loadingCompletedText = value;
this._onLoadingCompletedTextChange.fire(this._loadingCompletedText);
}
}
public registerCloseValidator(validator: CloseValidator): void { public registerCloseValidator(validator: CloseValidator): void {
this._closeValidator = validator; this._closeValidator = validator;
} }
@@ -247,6 +290,15 @@ export class Wizard {
private _message: DialogMessage | undefined; private _message: DialogMessage | undefined;
public displayPageTitles: boolean = false; public displayPageTitles: boolean = false;
public width: DialogWidth | undefined; public width: DialogWidth | undefined;
private _loading: boolean = false;
private _loadingText: string;
private _loadingCompletedText: string;
private _onLoadingChange = new Emitter<boolean | undefined>();
private _onLoadingTextChange = new Emitter<string | undefined>();
private _onLoadingCompletedTextChange = new Emitter<string | undefined>();
public readonly onLoadingChange = this._onLoadingChange.event;
public readonly onLoadingTextChange = this._onLoadingTextChange.event;
public readonly onLoadingCompletedTextChange = this._onLoadingCompletedTextChange.event;
constructor(public title: string, constructor(public title: string,
public readonly name: string, public readonly name: string,
@@ -329,4 +381,39 @@ export class Wizard {
this._message = value; this._message = value;
this._onMessageChange.fire(this._message); this._onMessageChange.fire(this._message);
} }
public get loading(): boolean {
return this._loading;
}
public set loading(value: boolean) {
if (this.loading !== value) {
this._loading = value;
this._onLoadingChange.fire(this._loading);
}
}
public get loadingText(): string | undefined {
return this._loadingText;
}
public set loadingText(value: string | undefined) {
if (this.loadingText !== value) {
this._loadingText = value;
this._onLoadingTextChange.fire(this._loadingText);
}
}
public get loadingCompletedText(): string | undefined {
return this._loadingCompletedText;
}
public set loadingCompletedText(value: string | undefined) {
if (this._loadingCompletedText !== value) {
this._loadingCompletedText = value;
this._onLoadingCompletedTextChange.fire(this._loadingCompletedText);
}
}
} }

View File

@@ -136,7 +136,10 @@ suite('MainThreadModelViewDialog Tests', () => {
okButton: okButtonHandle, okButton: okButtonHandle,
cancelButton: cancelButtonHandle, cancelButton: cancelButtonHandle,
customButtons: [button1Handle, button2Handle], customButtons: [button1Handle, button2Handle],
message: undefined message: undefined,
loading: false,
loadingText: undefined,
loadingCompletedText: undefined,
}; };
// Set up the wizard details // Set up the wizard details
@@ -183,7 +186,10 @@ suite('MainThreadModelViewDialog Tests', () => {
pages: [page1Handle, page2Handle], pages: [page1Handle, page2Handle],
message: undefined, message: undefined,
displayPageTitles: false, displayPageTitles: false,
width: 'wide' width: 'wide',
loading: false,
loadingText: undefined,
loadingCompletedText: undefined
}; };
// Register the buttons, tabs, and dialog // Register the buttons, tabs, and dialog