mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 01:25:38 -05:00
Add validation to model view components (#1356)
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!./flexContainer';
|
||||
|
||||
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
|
||||
import {
|
||||
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
|
||||
ViewChild, ElementRef, Injector, OnDestroy, OnInit
|
||||
} from '@angular/core';
|
||||
|
||||
@@ -18,14 +19,16 @@ import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class ItemDescriptor<T> {
|
||||
constructor(public descriptor: IComponentDescriptor, public config: T) {}
|
||||
constructor(public descriptor: IComponentDescriptor, public config: T) { }
|
||||
}
|
||||
|
||||
export abstract class ComponentBase extends Disposable implements IComponent, OnDestroy, OnInit {
|
||||
protected properties: { [key: string]: any; } = {};
|
||||
constructor (
|
||||
protected _valid: boolean = true;
|
||||
private _eventQueue: IComponentEventArgs[] = [];
|
||||
constructor(
|
||||
protected _changeRef: ChangeDetectorRef) {
|
||||
super();
|
||||
super();
|
||||
}
|
||||
|
||||
/// IComponent implementation
|
||||
@@ -56,7 +59,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
abstract setLayout (layout: any): void;
|
||||
abstract setLayout(layout: any): void;
|
||||
|
||||
public setProperties(properties: { [key: string]: any; }): void {
|
||||
if (!properties) {
|
||||
@@ -77,21 +80,49 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
|
||||
|
||||
protected setPropertyFromUI<TPropertyBag, TValue>(propertySetter: (TPropertyBag, TValue) => void, value: TValue) {
|
||||
propertySetter(this.getProperties<TPropertyBag>(), value);
|
||||
this._onEventEmitter.fire({
|
||||
this.fireEvent({
|
||||
eventType: ComponentEventType.PropertiesChanged,
|
||||
args: this.getProperties()
|
||||
});
|
||||
}
|
||||
|
||||
public get onEvent(): Event<IComponentEventArgs> {
|
||||
return this._onEventEmitter.event;
|
||||
}
|
||||
|
||||
public get title(): string {
|
||||
let properties = this.getProperties();
|
||||
let title = properties['title'];
|
||||
return title ? <string>title : '';
|
||||
}
|
||||
|
||||
public get valid(): boolean {
|
||||
return this._valid;
|
||||
}
|
||||
|
||||
public setValid(valid: boolean): void {
|
||||
if (this._valid !== valid) {
|
||||
this._valid = valid;
|
||||
this.fireEvent({
|
||||
eventType: ComponentEventType.validityChanged,
|
||||
args: valid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public registerEventHandler(handler: (event: IComponentEventArgs) => void): IDisposable {
|
||||
if (this._eventQueue) {
|
||||
while (this._eventQueue.length > 0) {
|
||||
let event = this._eventQueue.pop();
|
||||
handler(event);
|
||||
}
|
||||
this._eventQueue = undefined;
|
||||
}
|
||||
return this._onEventEmitter.event(handler);
|
||||
}
|
||||
|
||||
private fireEvent(event: IComponentEventArgs) {
|
||||
this._onEventEmitter.fire(event);
|
||||
if (this._eventQueue) {
|
||||
this._eventQueue.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ContainerBase<T> extends ComponentBase {
|
||||
@@ -115,5 +146,5 @@ export abstract class ContainerBase<T> extends ComponentBase {
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
abstract setLayout (layout: any): void;
|
||||
abstract setLayout(layout: any): void;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
|
||||
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
|
||||
import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { InputBox, IInputOptions, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { attachInputBoxStyler, attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
|
||||
@@ -44,7 +45,19 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
|
||||
if (this._inputContainer) {
|
||||
let inputOptions: IInputOptions = {
|
||||
placeholder: '',
|
||||
ariaLabel: ''
|
||||
ariaLabel: '',
|
||||
validationOptions: {
|
||||
validation: () => {
|
||||
if (this.valid) {
|
||||
return undefined;
|
||||
} else {
|
||||
return {
|
||||
content: nls.localize('invalidValueError', 'Invalid value'),
|
||||
type: MessageType.ERROR
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._input = new InputBox(this._inputContainer.nativeElement, this._commonService.contextViewService, inputOptions);
|
||||
@@ -81,6 +94,11 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
|
||||
this._input.value = this.value;
|
||||
}
|
||||
|
||||
public setValid(valid: boolean): void {
|
||||
super.setValid(valid);
|
||||
this._input.validate();
|
||||
}
|
||||
|
||||
// CSS-bound properties
|
||||
|
||||
public get value(): string {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { InjectionToken } from '@angular/core';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
/**
|
||||
* An instance of a model-backed component. This will be a UI element
|
||||
@@ -17,12 +18,14 @@ export interface IComponent {
|
||||
descriptor: IComponentDescriptor;
|
||||
modelStore: IModelStore;
|
||||
layout();
|
||||
registerEventHandler(handler: (event: IComponentEventArgs) => void): IDisposable;
|
||||
clearContainer?: () => void;
|
||||
addToContainer?: (componentDescriptor: IComponentDescriptor, config: any) => void;
|
||||
setLayout?: (layout: any) => void;
|
||||
setProperties?: (properties: { [key: string]: any; }) => void;
|
||||
readonly valid?: boolean;
|
||||
setValid(valid: boolean): void;
|
||||
title?: string;
|
||||
onEvent?: Event<IComponentEventArgs>;
|
||||
}
|
||||
|
||||
export const COMPONENT_CONFIG = new InjectionToken<IComponentConfig>('component_config');
|
||||
@@ -60,7 +63,8 @@ export interface IComponentEventArgs {
|
||||
export enum ComponentEventType {
|
||||
PropertiesChanged,
|
||||
onDidChange,
|
||||
onDidClick
|
||||
onDidClick,
|
||||
validityChanged
|
||||
}
|
||||
|
||||
export interface IModelStore {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IModelStore, IComponentDescriptor, IComponent } from './interfaces';
|
||||
import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
|
||||
const componentRegistry = <IComponentRegistry> Registry.as(Extensions.ComponentContribution);
|
||||
const componentRegistry = <IComponentRegistry>Registry.as(Extensions.ComponentContribution);
|
||||
|
||||
|
||||
class ComponentDescriptor implements IComponentDescriptor {
|
||||
|
||||
@@ -91,6 +91,10 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
|
||||
this.queueAction(componentId, (component) => component.setProperties(properties));
|
||||
}
|
||||
|
||||
setValid(componentId: string, valid: boolean): void {
|
||||
this.queueAction(componentId, (component) => component.setValid(valid));
|
||||
}
|
||||
|
||||
private queueAction<T>(componentId: string, action: (component: IComponent) => T): void {
|
||||
this.modelStore.eventuallyRunOnComponent(componentId, action).catch(err => {
|
||||
// TODO add error handling
|
||||
@@ -99,12 +103,10 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
|
||||
|
||||
registerEvent(componentId: string) {
|
||||
this.queueAction(componentId, (component) => {
|
||||
if (component.onEvent) {
|
||||
this._register(component.onEvent(e => {
|
||||
e.componentId = componentId;
|
||||
this._onEventEmitter.fire(e);
|
||||
}));
|
||||
}
|
||||
this._register(component.registerEventHandler(e => {
|
||||
e.componentId = componentId;
|
||||
this._onEventEmitter.fire(e);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.com
|
||||
import { BootstrapParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { BOOTSTRAP_SERVICE_ID, IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { ComponentEventType } from '../../parts/modelComponents/interfaces';
|
||||
|
||||
export interface DialogComponentParams extends BootstrapParams {
|
||||
modelViewId: string;
|
||||
validityChangedCallback: (valid: boolean) => void;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -27,16 +29,23 @@ export interface DialogComponentParams extends BootstrapParams {
|
||||
export class DialogContainer implements AfterContentInit {
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
private _params: DialogComponentParams;
|
||||
|
||||
public modelViewId: string;
|
||||
@ViewChild(ModelViewContent) private _modelViewContent: ModelViewContent;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) bootstrapService: IBootstrapService) {
|
||||
this.modelViewId = (bootstrapService.getBootstrapParams(el.nativeElement.tagName) as DialogComponentParams).modelViewId;
|
||||
this._params = bootstrapService.getBootstrapParams(el.nativeElement.tagName) as DialogComponentParams;
|
||||
this.modelViewId = this._params.modelViewId;
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._modelViewContent.onEvent(event => {
|
||||
if (event.eventType === ComponentEventType.validityChanged) {
|
||||
this._params.validityChangedCallback(event.args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
|
||||
@@ -21,6 +21,8 @@ import { attachButtonStyler } from 'vs/platform/theme/common/styler';
|
||||
import { Button } from 'vs/base/browser/ui/button/button';
|
||||
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { localize } from 'vs/nls';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
|
||||
export class DialogModal extends Modal {
|
||||
private _dialogPane: DialogPane;
|
||||
@@ -102,8 +104,10 @@ export class DialogModal extends Modal {
|
||||
}
|
||||
|
||||
public done(): void {
|
||||
this.dispose();
|
||||
this.hide();
|
||||
if (this._dialog.okButton.enabled) {
|
||||
this.dispose();
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
@@ -119,6 +123,20 @@ export class DialogModal extends Modal {
|
||||
super.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable to change behavior of escape key
|
||||
*/
|
||||
protected onClose(e: StandardKeyboardEvent) {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable to change behavior of enter key
|
||||
*/
|
||||
protected onAccept(e: StandardKeyboardEvent) {
|
||||
this.done();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._dialogPane.dispose();
|
||||
|
||||
@@ -16,12 +16,16 @@ import { DialogComponentParams } from 'sql/platform/dialog/dialogContainer.compo
|
||||
import { Builder } from 'vs/base/browser/builder';
|
||||
import { IThemable } from 'vs/platform/theme/common/styler';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export class DialogPane extends Disposable implements IThemable {
|
||||
private _activeTabIndex: number;
|
||||
private _tabbedPanel: TabbedPanel;
|
||||
private _moduleRef: NgModuleRef<{}>;
|
||||
|
||||
// Validation
|
||||
private _modelViewValidityMap = new Map<string, boolean>();
|
||||
|
||||
// HTML Elements
|
||||
private _body: HTMLElement;
|
||||
private _tabBar: HTMLElement;
|
||||
@@ -46,12 +50,20 @@ export class DialogPane extends Disposable implements IThemable {
|
||||
} else {
|
||||
this._tabbedPanel = new TabbedPanel(this._body);
|
||||
this._dialog.content.forEach((tab, tabIndex) => {
|
||||
let tabContainer = document.createElement('div');
|
||||
tabContainer.style.display = 'none';
|
||||
this._body.appendChild(tabContainer);
|
||||
this.initializeModelViewContainer(tabContainer, tab.content);
|
||||
this._tabbedPanel.pushTab({
|
||||
title: tab.title,
|
||||
identifier: 'dialogPane.' + this._dialog.title + '.' + tabIndex,
|
||||
view: {
|
||||
render: (container) => {
|
||||
this.initializeModelViewContainer(container, tab.content);
|
||||
if (tabContainer.parentElement === this._body) {
|
||||
this._body.removeChild(tabContainer);
|
||||
}
|
||||
container.appendChild(tabContainer);
|
||||
tabContainer.style.display = 'block';
|
||||
},
|
||||
layout: (dimension) => { }
|
||||
} as IPanelView
|
||||
@@ -72,7 +84,10 @@ export class DialogPane extends Disposable implements IThemable {
|
||||
DialogModule,
|
||||
bodyContainer,
|
||||
'dialog-modelview-container',
|
||||
{ modelViewId: modelViewId } as DialogComponentParams,
|
||||
{
|
||||
modelViewId: modelViewId,
|
||||
validityChangedCallback: (valid: boolean) => this._setValidity(modelViewId, valid)
|
||||
} as DialogComponentParams,
|
||||
undefined,
|
||||
(moduleRef) => this._moduleRef = moduleRef);
|
||||
}
|
||||
@@ -93,6 +108,21 @@ export class DialogPane extends Disposable implements IThemable {
|
||||
this._body.style.color = styles.dialogForeground ? styles.dialogForeground.toString() : undefined;
|
||||
}
|
||||
|
||||
private _setValidity(modelViewId: string, valid: boolean) {
|
||||
let oldValidity = this.isValid();
|
||||
this._modelViewValidityMap.set(modelViewId, valid);
|
||||
let newValidity = this.isValid();
|
||||
if (newValidity !== oldValidity) {
|
||||
this._dialog.notifyValidityChanged(newValidity);
|
||||
}
|
||||
}
|
||||
|
||||
private isValid(): boolean {
|
||||
let valid = true;
|
||||
this._modelViewValidityMap.forEach(value => valid = valid && value);
|
||||
return valid;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
super.dispose();
|
||||
this._moduleRef.destroy();
|
||||
|
||||
@@ -28,11 +28,24 @@ export class Dialog implements sqlops.window.modelviewdialog.Dialog {
|
||||
public cancelButton: DialogButton = new DialogButton(Dialog.CANCEL_BUTTON_LABEL, true);
|
||||
public customButtons: DialogButton[];
|
||||
|
||||
private _valid: boolean = true;
|
||||
private _validityChangedEmitter = new Emitter<boolean>();
|
||||
public readonly onValidityChanged = this._validityChangedEmitter.event;
|
||||
|
||||
constructor(public title: string, content?: string | DialogTab[]) {
|
||||
if (content) {
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
|
||||
public get valid(): boolean {
|
||||
return this._valid;
|
||||
}
|
||||
|
||||
public notifyValidityChanged(valid: boolean) {
|
||||
this._valid = valid;
|
||||
this._validityChangedEmitter.fire(valid);
|
||||
}
|
||||
}
|
||||
|
||||
export class DialogButton implements sqlops.window.modelviewdialog.Button {
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface IModelView extends IView {
|
||||
addToContainer(containerId: string, item: IItemConfig): void;
|
||||
setLayout(componentId: string, layout: any): void;
|
||||
setProperties(componentId: string, properties: { [key: string]: any }): void;
|
||||
setValid(componentId: string, valid: boolean): void;
|
||||
registerEvent(componentId: string);
|
||||
onEvent: Event<any>;
|
||||
}
|
||||
|
||||
41
src/sql/sqlops.proposed.d.ts
vendored
41
src/sql/sqlops.proposed.d.ts
vendored
@@ -31,6 +31,7 @@ declare module 'sqlops' {
|
||||
export interface ComponentBuilder<T extends Component> {
|
||||
component(): T;
|
||||
withProperties<U>(properties: U): ComponentBuilder<T>;
|
||||
withValidation(validation: (component: T) => boolean): ComponentBuilder<T>;
|
||||
}
|
||||
export interface ContainerBuilder<T extends Component, TLayout, TItemLayout> extends ComponentBuilder<T> {
|
||||
withLayout(layout: TLayout): ContainerBuilder<T, TLayout, TItemLayout>;
|
||||
@@ -56,6 +57,21 @@ declare module 'sqlops' {
|
||||
* @memberof Component
|
||||
*/
|
||||
updateProperties(properties: { [key: string]: any }): Thenable<boolean>;
|
||||
|
||||
/**
|
||||
* Event fired to notify that the component's validity has changed
|
||||
*/
|
||||
readonly onValidityChanged: vscode.Event<boolean>;
|
||||
|
||||
/**
|
||||
* Whether the component is valid or not
|
||||
*/
|
||||
readonly valid: boolean;
|
||||
|
||||
/**
|
||||
* Run the component's validations
|
||||
*/
|
||||
validate(): void;
|
||||
}
|
||||
|
||||
export interface FormComponent {
|
||||
@@ -264,6 +280,21 @@ declare module 'sqlops' {
|
||||
*/
|
||||
readonly modelBuilder: ModelBuilder;
|
||||
|
||||
/**
|
||||
* Whether or not the model view's root component is valid
|
||||
*/
|
||||
readonly valid: boolean;
|
||||
|
||||
/**
|
||||
* Raised when the model view's valid property changes
|
||||
*/
|
||||
readonly onValidityChanged: vscode.Event<boolean>;
|
||||
|
||||
/**
|
||||
* Run the model view root component's validations
|
||||
*/
|
||||
validate(): void;
|
||||
|
||||
/**
|
||||
* Initializes the model with a root component definition.
|
||||
* Once this has been done, the components will be laid out in the UI and
|
||||
@@ -336,6 +367,16 @@ declare module 'sqlops' {
|
||||
* Any additional buttons that should be displayed
|
||||
*/
|
||||
customButtons: Button[];
|
||||
|
||||
/**
|
||||
* Whether the dialog's content is valid
|
||||
*/
|
||||
readonly valid: boolean;
|
||||
|
||||
/**
|
||||
* Fired whenever the dialog's valid property changes
|
||||
*/
|
||||
readonly onValidityChanged: vscode.Event<boolean>;
|
||||
}
|
||||
|
||||
export interface DialogTab {
|
||||
|
||||
@@ -25,17 +25,23 @@ class ModelBuilderImpl implements sqlops.ModelBuilder {
|
||||
|
||||
navContainer(): sqlops.ContainerBuilder<sqlops.NavContainer, any, any> {
|
||||
let id = this.getNextComponentId();
|
||||
return new ContainerBuilderImpl(this._proxy, this._handle, ModelComponentTypes.NavContainer, id);
|
||||
let container: ContainerBuilderImpl<sqlops.NavContainer, any, any> = new ContainerBuilderImpl(this._proxy, this._handle, ModelComponentTypes.NavContainer, id);
|
||||
this._eventHandlers.set(id, container);
|
||||
return container;
|
||||
}
|
||||
|
||||
flexContainer(): sqlops.FlexBuilder {
|
||||
let id = this.getNextComponentId();
|
||||
return new ContainerBuilderImpl<sqlops.FlexContainer, sqlops.FlexLayout, sqlops.FlexItemLayout>(this._proxy, this._handle, ModelComponentTypes.FlexContainer, id);
|
||||
let container: ContainerBuilderImpl<sqlops.FlexContainer, any, any> = new ContainerBuilderImpl<sqlops.FlexContainer, sqlops.FlexLayout, sqlops.FlexItemLayout>(this._proxy, this._handle, ModelComponentTypes.FlexContainer, id);
|
||||
this._eventHandlers.set(id, container);
|
||||
return container;
|
||||
}
|
||||
|
||||
formContainer(): sqlops.FormBuilder {
|
||||
let id = this.getNextComponentId();
|
||||
return new FormContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Form, id);
|
||||
let container = new FormContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Form, id);
|
||||
this._eventHandlers.set(id, container);
|
||||
return container;
|
||||
}
|
||||
|
||||
card(): sqlops.ComponentBuilder<sqlops.CardComponent> {
|
||||
@@ -110,6 +116,11 @@ class ComponentBuilderImpl<T extends sqlops.Component> implements sqlops.Compone
|
||||
return this;
|
||||
}
|
||||
|
||||
withValidation(validation: (component: T) => boolean): sqlops.ComponentBuilder<T> {
|
||||
this._component.validations.push(validation);
|
||||
return this;
|
||||
}
|
||||
|
||||
handleEvent(eventArgs: IComponentEventArgs) {
|
||||
this._component.onEvent(eventArgs);
|
||||
}
|
||||
@@ -138,6 +149,7 @@ class ContainerBuilderImpl<T extends sqlops.Component, TLayout, TItemLayout> ext
|
||||
let componentWrapper = item as ComponentWrapper;
|
||||
return new InternalItemConfig(componentWrapper, itemLayout);
|
||||
});
|
||||
components.forEach(component => component.onValidityChanged(() => this._component.validate()));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -169,6 +181,7 @@ class FormContainerBuilder extends ContainerBuilderImpl<sqlops.FormContainer, sq
|
||||
this._component.itemConfigs.push(new InternalItemConfig(componentWrapper, itemLayout));
|
||||
});
|
||||
}
|
||||
formItem.component.onValidityChanged(() => this._component.validate());
|
||||
});
|
||||
return this;
|
||||
}
|
||||
@@ -194,6 +207,10 @@ class ComponentWrapper implements sqlops.Component {
|
||||
public properties: { [key: string]: any } = {};
|
||||
public layout: any;
|
||||
public itemConfigs: InternalItemConfig[];
|
||||
public validations: ((component: ThisType<ComponentWrapper>) => boolean)[] = [];
|
||||
private _valid: boolean = true;
|
||||
private _onValidityChangedEmitter = new Emitter<boolean>();
|
||||
public readonly onValidityChanged = this._onValidityChangedEmitter.event;
|
||||
|
||||
private _onErrorEmitter = new Emitter<Error>();
|
||||
public readonly onError: vscode.Event<Error> = this._onErrorEmitter.event;
|
||||
@@ -206,6 +223,12 @@ class ComponentWrapper implements sqlops.Component {
|
||||
) {
|
||||
this.properties = {};
|
||||
this.itemConfigs = [];
|
||||
this.validations.push((component: this) => {
|
||||
return component.items.every(item => {
|
||||
item.validate();
|
||||
return item.valid;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
@@ -248,6 +271,7 @@ class ComponentWrapper implements sqlops.Component {
|
||||
}
|
||||
let config = new InternalItemConfig(itemImpl, itemLayout);
|
||||
this.itemConfigs.push(config);
|
||||
itemImpl.onValidityChanged(() => this.validate());
|
||||
this._proxy.$addToContainer(this._handle, this.id, config.toIItemConfig()).then(undefined, this.handleError);
|
||||
}
|
||||
|
||||
@@ -270,18 +294,20 @@ class ComponentWrapper implements sqlops.Component {
|
||||
public onEvent(eventArgs: IComponentEventArgs) {
|
||||
if (eventArgs && eventArgs.eventType === ComponentEventType.PropertiesChanged) {
|
||||
this.properties = eventArgs.args;
|
||||
this.validate();
|
||||
} else if (eventArgs) {
|
||||
let emitter = this._emitterMap.get(eventArgs.eventType);
|
||||
if (emitter) {
|
||||
emitter.fire();
|
||||
}
|
||||
let emitter = this._emitterMap.get(eventArgs.eventType);
|
||||
if (emitter) {
|
||||
emitter.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected setProperty(key: string, value: any): Thenable<boolean> {
|
||||
if (!this.properties[key] || this.properties[key] !== value) {
|
||||
// Only notify the front end if a value has been updated
|
||||
this.properties[key] = value;
|
||||
this.validate();
|
||||
return this.notifyPropertyChanged();
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
@@ -290,6 +316,29 @@ class ComponentWrapper implements sqlops.Component {
|
||||
private handleError(err: Error): void {
|
||||
this._onErrorEmitter.fire(err);
|
||||
}
|
||||
|
||||
public validate(): void {
|
||||
let isValid = true;
|
||||
try {
|
||||
this.validations.forEach(validation => {
|
||||
if (!validation(this)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
isValid = false;
|
||||
}
|
||||
let oldValid = this._valid;
|
||||
if (this._valid !== isValid) {
|
||||
this._valid = isValid;
|
||||
this._proxy.$notifyValidation(this._handle, this._id, isValid);
|
||||
this._onValidityChangedEmitter.fire(this._valid);
|
||||
}
|
||||
}
|
||||
|
||||
public get valid(): boolean {
|
||||
return this._valid;
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerWrapper<T, U> extends ComponentWrapper implements sqlops.Container<T, U> {
|
||||
@@ -428,8 +477,11 @@ class ButtonWrapper extends ComponentWrapper implements sqlops.ButtonComponent {
|
||||
class ModelViewImpl implements sqlops.ModelView {
|
||||
|
||||
public onClosedEmitter = new Emitter<any>();
|
||||
private _onValidityChangedEmitter = new Emitter<boolean>();
|
||||
public readonly onValidityChanged = this._onValidityChangedEmitter.event;
|
||||
|
||||
private _modelBuilder: ModelBuilderImpl;
|
||||
private _component: sqlops.Component;
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: MainThreadModelViewShape,
|
||||
@@ -456,17 +508,28 @@ class ModelViewImpl implements sqlops.ModelView {
|
||||
return this._modelBuilder;
|
||||
}
|
||||
|
||||
public get valid(): boolean {
|
||||
return this._component.valid;
|
||||
}
|
||||
|
||||
public handleEvent(componentId: string, eventArgs: IComponentEventArgs): void {
|
||||
this._modelBuilder.handleEvent(componentId, eventArgs);
|
||||
}
|
||||
|
||||
public initializeModel<T extends sqlops.Component>(component: T): Thenable<void> {
|
||||
component.onValidityChanged(valid => this._onValidityChangedEmitter.fire(valid));
|
||||
this._component = component;
|
||||
let componentImpl = <any>component as ComponentWrapper;
|
||||
if (!componentImpl) {
|
||||
return Promise.reject(nls.localize('unknownConfig', 'Unkown component configuration, must use ModelBuilder to create a configuration object'));
|
||||
}
|
||||
componentImpl.validate();
|
||||
return this._proxy.$initializeModel(this._handle, componentImpl.toComponentShape());
|
||||
}
|
||||
|
||||
public validate(): void {
|
||||
this._component.validate();
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtHostModelView implements ExtHostModelViewShape {
|
||||
|
||||
@@ -21,10 +21,18 @@ class DialogImpl implements sqlops.window.modelviewdialog.Dialog {
|
||||
public okButton: sqlops.window.modelviewdialog.Button;
|
||||
public cancelButton: sqlops.window.modelviewdialog.Button;
|
||||
public customButtons: sqlops.window.modelviewdialog.Button[];
|
||||
public readonly onValidityChanged: vscode.Event<boolean>;
|
||||
private _valid: boolean = true;
|
||||
|
||||
constructor(private _extHostModelViewDialog: ExtHostModelViewDialog) {
|
||||
this.okButton = this._extHostModelViewDialog.createButton(nls.localize('dialogOkLabel', 'Done'));
|
||||
this.cancelButton = this._extHostModelViewDialog.createButton(nls.localize('dialogCancelLabel', 'Cancel'));
|
||||
this.onValidityChanged = this._extHostModelViewDialog.getValidityChangedEvent(this);
|
||||
this.onValidityChanged(valid => this._valid = valid);
|
||||
}
|
||||
|
||||
public get valid(): boolean {
|
||||
return this._valid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +97,7 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
|
||||
private readonly _tabHandles = new Map<sqlops.window.modelviewdialog.DialogTab, number>();
|
||||
private readonly _buttonHandles = new Map<sqlops.window.modelviewdialog.Button, number>();
|
||||
|
||||
private readonly _validityEmitters = new Map<number, Emitter<boolean>>();
|
||||
private readonly _onClickCallbacks = new Map<number, () => void>();
|
||||
|
||||
constructor(
|
||||
@@ -134,6 +143,13 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
|
||||
this._onClickCallbacks.get(handle)();
|
||||
}
|
||||
|
||||
public $onDialogValidityChanged(handle: number, valid: boolean): void {
|
||||
let emitter = this._validityEmitters.get(handle);
|
||||
if (emitter) {
|
||||
emitter.fire(valid);
|
||||
}
|
||||
}
|
||||
|
||||
public open(dialog: sqlops.window.modelviewdialog.Dialog): void {
|
||||
let handle = this.getDialogHandle(dialog);
|
||||
this.updateDialogContent(dialog);
|
||||
@@ -208,4 +224,14 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
|
||||
button.label = label;
|
||||
return button;
|
||||
}
|
||||
|
||||
public getValidityChangedEvent(dialog: sqlops.window.modelviewdialog.Dialog) {
|
||||
let handle = this.getDialogHandle(dialog);
|
||||
let emitter = this._validityEmitters.get(handle);
|
||||
if (!emitter) {
|
||||
emitter = new Emitter<boolean>();
|
||||
this._validityEmitters.set(handle, emitter);
|
||||
}
|
||||
return emitter.event;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,10 @@ export class MainThreadModelView extends Disposable implements MainThreadModelVi
|
||||
return this.execModelViewAction(handle, (modelView) => modelView.setProperties(componentId, properties));
|
||||
}
|
||||
|
||||
$notifyValidation(handle: number, componentId: string, valid: boolean): Thenable<void> {
|
||||
return this.execModelViewAction(handle, (modelView) => modelView.setValid(componentId, valid));
|
||||
}
|
||||
|
||||
private execModelViewAction<T>(handle: number, action: (m: IModelView) => T): Thenable<T> {
|
||||
let modelView: IModelView = this._dialogs.get(handle);
|
||||
let result = action(modelView);
|
||||
|
||||
@@ -52,6 +52,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
|
||||
let cancelButton = this.getButton(details.cancelButton);
|
||||
dialog.okButton = okButton;
|
||||
dialog.cancelButton = cancelButton;
|
||||
dialog.onValidityChanged(valid => this._proxy.$onDialogValidityChanged(handle, valid));
|
||||
this._dialogs.set(handle, dialog);
|
||||
}
|
||||
|
||||
|
||||
@@ -530,6 +530,7 @@ export interface MainThreadModelViewShape extends IDisposable {
|
||||
$setLayout(handle: number, componentId: string, layout: any): Thenable<void>;
|
||||
$setProperties(handle: number, componentId: string, properties: { [key: string]: any }): Thenable<void>;
|
||||
$registerEvent(handle: number, componentId: string): Thenable<void>;
|
||||
$notifyValidation(handle: number, componentId: string, valid: boolean): Thenable<void>;
|
||||
}
|
||||
|
||||
export interface ExtHostObjectExplorerShape {
|
||||
@@ -547,6 +548,7 @@ export interface MainThreadObjectExplorerShape extends IDisposable {
|
||||
|
||||
export interface ExtHostModelViewDialogShape {
|
||||
$onButtonClick(handle: number): void;
|
||||
$onDialogValidityChanged(handle: number, valid: boolean): void;
|
||||
}
|
||||
|
||||
export interface MainThreadModelViewDialogShape extends IDisposable {
|
||||
|
||||
Reference in New Issue
Block a user