Run initial modelview actions first (#13317)

* Run initial modelview actions first

* add param

* Comments and typings

* Catch promise error

* fix db projects test

* remove extra calls
This commit is contained in:
Charles Gagnon
2020-11-10 17:00:16 -08:00
committed by GitHub
parent b4dd0442c5
commit b83da2dfa8
6 changed files with 70 additions and 42 deletions

View File

@@ -29,10 +29,8 @@ describe('Add Database Reference Dialog', () => {
const dialog = new AddDatabaseReferenceDialog(project); const dialog = new AddDatabaseReferenceDialog(project);
await dialog.openDialog(); await dialog.openDialog();
should(dialog.dialog.okButton.enabled).equal(false); should(dialog.dialog.okButton.enabled).equal(true, 'Ok button should be enabled since initial type of systemDb has default values filled');
should(dialog.currentReferenceType).equal(ReferenceType.systemDb); should(dialog.currentReferenceType).equal(ReferenceType.systemDb);
dialog.tryEnableAddReferenceButton();
should(dialog.dialog.okButton.enabled).equal(true, 'Ok button should be enabled because there is a default value in the database name textbox');
// empty db name textbox // empty db name textbox
dialog.databaseNameTextbox!.value = ''; dialog.databaseNameTextbox!.value = '';

View File

@@ -69,7 +69,7 @@ export interface IModelStore {
* @param componentId unique identifier of the component * @param componentId unique identifier of the component
* @param action some action to perform * @param action some action to perform
*/ */
eventuallyRunOnComponent<T>(componentId: string, action: (component: IComponent) => T): Promise<T>; eventuallyRunOnComponent<T>(componentId: string, action: (component: IComponent) => T, initial: boolean): void;
/** /**
* Register a callback that will validate components when given a component ID * Register a callback that will validate components when given a component ID
*/ */

View File

@@ -35,12 +35,12 @@ export interface IModelView extends IView {
clearContainer(componentId: string): void; clearContainer(componentId: string): void;
addToContainer(containerId: string, item: IItemConfig, index?: number): void; addToContainer(containerId: string, item: IItemConfig, index?: number): void;
removeFromContainer(containerId: string, item: IItemConfig): void; removeFromContainer(containerId: string, item: IItemConfig): void;
setLayout(componentId: string, layout: any): void; setLayout(componentId: string, layout: any, initial?: boolean): void;
setItemLayout(componentId: string, item: IItemConfig): void; setItemLayout(componentId: string, item: IItemConfig): void;
setProperties(componentId: string, properties: { [key: string]: any }): void; setProperties(componentId: string, properties: { [key: string]: any }, initial?: boolean): void;
setDataProvider(handle: number, componentId: string, context: any): void; setDataProvider(handle: number, componentId: string, context: any): void;
refreshDataProvider(componentId: string, item: any): void; refreshDataProvider(componentId: string, item: any): void;
registerEvent(componentId: string): void; registerEvent(componentId: string, initial?: boolean): void;
onEvent: Event<IModelViewEventArgs>; onEvent: Event<IModelViewEventArgs>;
validate(componentId: string): Thenable<boolean>; validate(componentId: string): Thenable<boolean>;
readonly onDestroy: Event<void>; readonly onDestroy: Event<void>;

View File

@@ -308,7 +308,7 @@ export abstract class ContainerBase<T, TPropertyBag extends azdata.ComponentProp
if (event.eventType === ComponentEventType.validityChanged) { if (event.eventType === ComponentEventType.validityChanged) {
this.validate(); this.validate();
} }
})); }), false);
this._changeRef.detectChanges(); this._changeRef.detectChanges();
this.onItemsUpdated(); this.onItemsUpdated();
return; return;

View File

@@ -6,6 +6,7 @@
import { Deferred } from 'sql/base/common/promise'; import { Deferred } from 'sql/base/common/promise';
import { entries } from 'sql/base/common/collections'; import { entries } from 'sql/base/common/collections';
import { IComponentDescriptor, IModelStore, IComponent } from 'sql/platform/dashboard/browser/interfaces'; import { IComponentDescriptor, IModelStore, IComponent } from 'sql/platform/dashboard/browser/interfaces';
import { onUnexpectedError } from 'vs/base/common/errors';
class ComponentDescriptor implements IComponentDescriptor { class ComponentDescriptor implements IComponentDescriptor {
constructor(public readonly id: string, public readonly type: string) { constructor(public readonly id: string, public readonly type: string) {
@@ -13,11 +14,13 @@ class ComponentDescriptor implements IComponentDescriptor {
} }
} }
export type ModelStoreAction<T> = (component: IComponent) => T;
export class ModelStore implements IModelStore { export class ModelStore implements IModelStore {
private _descriptorMappings: { [x: string]: IComponentDescriptor } = {}; private _descriptorMappings: { [x: string]: IComponentDescriptor } = {};
private _componentMappings: { [x: string]: IComponent } = {}; private _componentMappings: { [x: string]: IComponent } = {};
private _componentActions: { [x: string]: Deferred<IComponent> } = {}; private _componentActions: { [x: string]: { initial: ModelStoreAction<any>[], actions: Deferred<IComponent> } } = {};
private _validationCallbacks: ((componentId: string) => Thenable<boolean>)[] = []; private _validationCallbacks: ((componentId: string) => Thenable<boolean>)[] = [];
constructor() { constructor() {
} }
@@ -50,12 +53,19 @@ export class ModelStore implements IModelStore {
return this._componentMappings[componentId]; return this._componentMappings[componentId];
} }
eventuallyRunOnComponent<T>(componentId: string, action: (component: IComponent) => T): Promise<T> { /**
* Queues up an action to run once a component is created and registered. This will run immediately if the component is
* already registered.
* @param componentId The ID of the component to queue up the action for
* @param action The action to run when the component is registered
* @param initial Whether this is an initial setup action that should be done before other post-setup actions
*/
eventuallyRunOnComponent<T>(componentId: string, action: (component: IComponent) => T, initial: boolean = false): void {
let component = this.getComponent(componentId); let component = this.getComponent(componentId);
if (component) { if (component) {
return Promise.resolve(action(component)); action(component);
} else { } else {
return this.addPendingAction(componentId, action); this.addPendingAction(componentId, action, initial);
} }
} }
@@ -68,24 +78,46 @@ export class ModelStore implements IModelStore {
return Promise.all(this._validationCallbacks.map(callback => callback(componentId))).then(validations => validations.every(validation => validation === true)); return Promise.all(this._validationCallbacks.map(callback => callback(componentId))).then(validations => validations.every(validation => validation === true));
} }
private addPendingAction<T>(componentId: string, action: (component: IComponent) => T): Promise<T> { /**
* Adds the specified action to the list of actions to run once the specified component is created and registered.
* @param componentId The ID of the component to add the action to
* @param action The action to run once the component is registered
* @param initial Whether this is an initial setup action that should be ran before other post-setup actions
*/
private addPendingAction<T>(componentId: string, action: ModelStoreAction<T>, initial: boolean): void {
// We create a promise and chain it onto a tracking promise whose resolve method // We create a promise and chain it onto a tracking promise whose resolve method
// will only be called once the component is created // will only be called once the component is created
let deferredPromise = this._componentActions[componentId]; let deferredActions = this._componentActions[componentId];
if (!deferredPromise) { if (!deferredActions) {
deferredPromise = new Deferred(); deferredActions = { initial: [], actions: new Deferred() };
this._componentActions[componentId] = deferredPromise; this._componentActions[componentId] = deferredActions;
}
if (initial) {
deferredActions.initial.push(action);
} else {
deferredActions.actions.promise.then((component) => {
return action(component);
});
} }
let promise = deferredPromise.promise.then((component) => {
return action(component);
});
return promise;
} }
/**
* Runs the set of pending actions for a given component. This will run the initial setup actions
* first and then run all the other actions afterwards.
* @param componentId The ID of the component to run the currently pending actions for
* @param component The component object to run the actions against
*/
private runPendingActions(componentId: string, component: IComponent) { private runPendingActions(componentId: string, component: IComponent) {
let promiseTracker = this._componentActions[componentId]; let promiseTracker = this._componentActions[componentId];
if (promiseTracker) { if (promiseTracker) {
promiseTracker.resolve(component); // Run initial actions first to ensure they're done before later actions (and thus don't overwrite following actions)
new Promise(resolve => {
promiseTracker.initial.forEach(action => action(component));
resolve();
}).then(() => {
promiseTracker.actions.resolve(component);
}).catch(onUnexpectedError);
this._componentActions[componentId] = undefined;
} }
} }
} }

View File

@@ -60,12 +60,12 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
throw new Error(nls.localize('componentTypeNotRegistered', "Could not find component for type {0}", ModelComponentTypes[component.type])); throw new Error(nls.localize('componentTypeNotRegistered', "Could not find component for type {0}", ModelComponentTypes[component.type]));
} }
let descriptor = this.modelStore.createComponentDescriptor(typeId, component.id); let descriptor = this.modelStore.createComponentDescriptor(typeId, component.id);
this.setProperties(component.id, component.properties); this.setProperties(component.id, component.properties, true);
this.setLayout(component.id, component.layout); this.setLayout(component.id, component.layout, true);
this.registerEvent(component.id); this.registerEvent(component.id, true);
if (component.itemConfigs) { if (component.itemConfigs) {
for (let item of component.itemConfigs) { for (let item of component.itemConfigs) {
this.addToContainer(component.id, item); this.addToContainer(component.id, item, undefined, true);
} }
} }
@@ -84,12 +84,12 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
this.queueAction(componentId, (component) => component.clearContainer()); this.queueAction(componentId, (component) => component.clearContainer());
} }
addToContainer(containerId: string, itemConfig: IItemConfig, index?: number): void { addToContainer(containerId: string, itemConfig: IItemConfig, index?: number, initial: boolean = false): void {
// Do not return the promise as this should be non-blocking // Do not return the promise as this should be non-blocking
this.queueAction(containerId, (component) => { this.queueAction(containerId, (component) => {
let childDescriptor = this.defineComponent(itemConfig.componentShape); let childDescriptor = this.defineComponent(itemConfig.componentShape);
component.addToContainer(childDescriptor, itemConfig.config, index); component.addToContainer(childDescriptor, itemConfig.config, index);
}); }, initial);
} }
removeFromContainer(containerId: string, itemConfig: IItemConfig): void { removeFromContainer(containerId: string, itemConfig: IItemConfig): void {
@@ -100,11 +100,11 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
}); });
} }
setLayout(componentId: string, layout: any): void { setLayout(componentId: string, layout: any, initial: boolean = false): void {
if (!layout) { if (!layout) {
return; return;
} }
this.queueAction(componentId, (component) => component.setLayout(layout)); this.queueAction(componentId, (component) => component.setLayout(layout), initial);
} }
setItemLayout(containerId: string, itemConfig: IItemConfig): void { setItemLayout(containerId: string, itemConfig: IItemConfig): void {
@@ -114,24 +114,22 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
}); });
} }
setProperties(componentId: string, properties: { [key: string]: any; }): void { setProperties(componentId: string, properties: { [key: string]: any; }, initial: boolean = false): void {
if (!properties) { if (!properties) {
return; return;
} }
this.queueAction(componentId, (component) => component.setProperties(properties)); this.queueAction(componentId, (component) => component.setProperties(properties), initial);
} }
refreshDataProvider(componentId: string, item: any): void { refreshDataProvider(componentId: string, item: any): void {
this.queueAction(componentId, (component) => component.refreshDataProvider(item)); this.queueAction(componentId, (component) => component.refreshDataProvider(item));
} }
private queueAction<T>(componentId: string, action: (component: IComponent) => T): void { private queueAction<T>(componentId: string, action: (component: IComponent) => T, initial: boolean = false): void {
this.modelStore.eventuallyRunOnComponent(componentId, action).catch(err => { this.modelStore.eventuallyRunOnComponent(componentId, action, initial);
// TODO add error handling
});
} }
registerEvent(componentId: string) { registerEvent(componentId: string, initial: boolean = false) {
this.queueAction(componentId, (component) => { this.queueAction(componentId, (component) => {
this._register(component.registerEventHandler(e => { this._register(component.registerEventHandler(e => {
let modelViewEvent: IModelViewEventArgs = assign({ let modelViewEvent: IModelViewEventArgs = assign({
@@ -140,7 +138,7 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
}, e); }, e);
this._onEventEmitter.fire(modelViewEvent); this._onEventEmitter.fire(modelViewEvent);
})); }));
}); }, initial);
} }
public get onEvent(): Event<IModelViewEventArgs> { public get onEvent(): Event<IModelViewEventArgs> {
@@ -148,18 +146,18 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
} }
public validate(componentId: string): Thenable<boolean> { public validate(componentId: string): Thenable<boolean> {
return new Promise(resolve => this.modelStore.eventuallyRunOnComponent(componentId, component => resolve(component.validate()))); return new Promise(resolve => this.modelStore.eventuallyRunOnComponent(componentId, component => resolve(component.validate()), false));
} }
public setDataProvider(handle: number, componentId: string, context: any): any { public setDataProvider(handle: number, componentId: string, context: any): any {
return this.queueAction(componentId, (component) => component.setDataProvider(handle, componentId, context)); return this.queueAction(componentId, (component) => component.setDataProvider(handle, componentId, context), false);
} }
public focus(componentId: string): void { public focus(componentId: string): void {
return this.queueAction(componentId, (component) => component.focus()); return this.queueAction(componentId, (component) => component.focus(), false);
} }
public doAction(componentId: string, action: string, ...args: any[]): void { public doAction(componentId: string, action: string, ...args: any[]): void {
return this.queueAction(componentId, (component) => component.doAction(action, ...args)); return this.queueAction(componentId, (component) => component.doAction(action, ...args), false);
} }
} }