mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Add grouping feature for model view forms (#1853)
This commit is contained in:
@@ -22,13 +22,15 @@ import { getContentHeight, getContentWidth, Dimension } from 'vs/base/browser/do
|
|||||||
export interface TitledFormItemLayout {
|
export interface TitledFormItemLayout {
|
||||||
title: string;
|
title: string;
|
||||||
actions?: string[];
|
actions?: string[];
|
||||||
isFormComponent: Boolean;
|
isFormComponent: boolean;
|
||||||
horizontal: boolean;
|
horizontal: boolean;
|
||||||
componentWidth?: number | string;
|
componentWidth?: number | string;
|
||||||
componentHeight?: number | string;
|
componentHeight?: number | string;
|
||||||
titleFontSize?: number | string;
|
titleFontSize?: number | string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
info?: string;
|
info?: string;
|
||||||
|
isInGroup?: boolean;
|
||||||
|
isGroupLabel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormLayout {
|
export interface FormLayout {
|
||||||
@@ -43,10 +45,16 @@ class FormItem {
|
|||||||
template: `
|
template: `
|
||||||
<div #container *ngIf="items" class="form-table" [style.padding]="getFormPadding()" [style.width]="getFormWidth()" [style.height]="getFormHeight()">
|
<div #container *ngIf="items" class="form-table" [style.padding]="getFormPadding()" [style.width]="getFormWidth()" [style.height]="getFormHeight()">
|
||||||
<ng-container *ngFor="let item of items">
|
<ng-container *ngFor="let item of items">
|
||||||
|
<div class="form-row" *ngIf="isGroupLabel(item)">
|
||||||
|
<div class="form-item-row form-group-label">
|
||||||
|
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
|
||||||
|
</model-component-wrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-row" *ngIf="isFormComponent(item)" [style.height]="getRowHeight(item)">
|
<div class="form-row" *ngIf="isFormComponent(item)" [style.height]="getRowHeight(item)">
|
||||||
|
|
||||||
<ng-container *ngIf="isHorizontal(item)">
|
<ng-container *ngIf="isHorizontal(item)">
|
||||||
<div class="form-cell" [style.font-size]="getItemTitleFontSize(item)">
|
<div class="form-cell" [style.font-size]="getItemTitleFontSize(item)" [ngClass]="{'form-group-item': isInGroup(item)}">
|
||||||
{{getItemTitle(item)}}<span class="form-required" *ngIf="isItemRequired(item)">*</span>
|
{{getItemTitle(item)}}<span class="form-required" *ngIf="isItemRequired(item)">*</span>
|
||||||
<span class="icon info form-info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
|
<span class="icon info form-info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,7 +73,7 @@ class FormItem {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="form-vertical-container" *ngIf="isVertical(item)" [style.height]="getRowHeight(item)">
|
<div class="form-vertical-container" *ngIf="isVertical(item)" [style.height]="getRowHeight(item)" [ngClass]="{'form-group-item': isInGroup(item)}">
|
||||||
<div class="form-item-row" [style.font-size]="getItemTitleFontSize(item)">
|
<div class="form-item-row" [style.font-size]="getItemTitleFontSize(item)">
|
||||||
{{getItemTitle(item)}}<span class="form-required" *ngIf="isItemRequired(item)">*</span>
|
{{getItemTitle(item)}}<span class="form-required" *ngIf="isItemRequired(item)">*</span>
|
||||||
<span class="icon info form-info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
|
<span class="icon info form-info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
|
||||||
@@ -190,11 +198,19 @@ export default class FormContainer extends ContainerBase<FormItemLayout> impleme
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private isFormComponent(item: FormItem): Boolean {
|
private isGroupLabel(item: FormItem): boolean {
|
||||||
|
return item && item.config && item.config.isGroupLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isInGroup(item: FormItem): boolean {
|
||||||
|
return item && item.config && item.config.isInGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isFormComponent(item: FormItem): boolean {
|
||||||
return item && item.config && item.config.isFormComponent;
|
return item && item.config && item.config.isFormComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private itemHasActions(item: FormItem): Boolean {
|
private itemHasActions(item: FormItem): boolean {
|
||||||
let itemConfig = item.config;
|
let itemConfig = item.config;
|
||||||
return itemConfig && itemConfig.actions !== undefined && itemConfig.actions.length > 0;
|
return itemConfig && itemConfig.actions !== undefined && itemConfig.actions.length > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,3 +56,12 @@
|
|||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group-item .form-item-row,
|
||||||
|
.form-group-item.form-cell {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-label {
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
21
src/sql/sqlops.proposed.d.ts
vendored
21
src/sql/sqlops.proposed.d.ts
vendored
@@ -82,7 +82,7 @@ declare module 'sqlops' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FormBuilder extends ContainerBuilder<FormContainer, FormLayout, FormItemLayout> {
|
export interface FormBuilder extends ContainerBuilder<FormContainer, FormLayout, FormItemLayout> {
|
||||||
withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder<FormContainer, FormLayout, FormItemLayout>;
|
withFormItems(components: (FormComponent | FormComponentGroup)[], itemLayout?: FormItemLayout): FormBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a collection of child components and adds them all to this container
|
* Creates a collection of child components and adds them all to this container
|
||||||
@@ -90,7 +90,7 @@ declare module 'sqlops' {
|
|||||||
* @param formComponents the definitions
|
* @param formComponents the definitions
|
||||||
* @param {*} [itemLayout] Optional layout for the child items
|
* @param {*} [itemLayout] Optional layout for the child items
|
||||||
*/
|
*/
|
||||||
addFormItems(formComponents: Array<FormComponent>, itemLayout?: FormItemLayout): void;
|
addFormItems(formComponents: Array<FormComponent | FormComponentGroup>, itemLayout?: FormItemLayout): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a child component and adds it to this container.
|
* Creates a child component and adds it to this container.
|
||||||
@@ -98,7 +98,7 @@ declare module 'sqlops' {
|
|||||||
* @param formComponent the component to be added
|
* @param formComponent the component to be added
|
||||||
* @param {*} [itemLayout] Optional layout for this child item
|
* @param {*} [itemLayout] Optional layout for this child item
|
||||||
*/
|
*/
|
||||||
addFormItem(formComponent: FormComponent, itemLayout?: FormItemLayout): void;
|
addFormItem(formComponent: FormComponent | FormComponentGroup, itemLayout?: FormItemLayout): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Component {
|
export interface Component {
|
||||||
@@ -137,6 +137,21 @@ declare module 'sqlops' {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to create a group of components in a form layout
|
||||||
|
*/
|
||||||
|
export interface FormComponentGroup {
|
||||||
|
/**
|
||||||
|
* The form components to display in the group along with optional layouts for each item
|
||||||
|
*/
|
||||||
|
components: (FormComponent & { layout?: FormItemLayout })[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of the group, displayed above its components
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ToolbarComponent {
|
export interface ToolbarComponent {
|
||||||
component: Component;
|
component: Component;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class ModelBuilderImpl implements sqlops.ModelBuilder {
|
|||||||
|
|
||||||
formContainer(): sqlops.FormBuilder {
|
formContainer(): sqlops.FormBuilder {
|
||||||
let id = this.getNextComponentId();
|
let id = this.getNextComponentId();
|
||||||
let container = new FormContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Form, id);
|
let container = new FormContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Form, id, this);
|
||||||
this._componentBuilders.set(id, container);
|
this._componentBuilders.set(id, container);
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
@@ -249,14 +249,12 @@ class ContainerBuilderImpl<T extends sqlops.Component, TLayout, TItemLayout> ext
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FormContainerBuilder extends ContainerBuilderImpl<sqlops.FormContainer, sqlops.FormLayout, sqlops.FormItemLayout> implements sqlops.FormBuilder {
|
class FormContainerBuilder extends ContainerBuilderImpl<sqlops.FormContainer, sqlops.FormLayout, sqlops.FormItemLayout> implements sqlops.FormBuilder {
|
||||||
withFormItems(components: sqlops.FormComponent[], itemLayout?: sqlops.FormItemLayout): sqlops.ContainerBuilder<sqlops.FormContainer, sqlops.FormLayout, sqlops.FormItemLayout> {
|
constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string, private _builder: ModelBuilderImpl) {
|
||||||
this._component.itemConfigs = components.map(item => {
|
super(proxy, handle, type, id);
|
||||||
return this.convertToItemConfig(item, itemLayout);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
components.forEach(formItem => {
|
withFormItems(components: (sqlops.FormComponent | sqlops.FormComponentGroup)[], itemLayout?: sqlops.FormItemLayout): sqlops.FormBuilder {
|
||||||
this.addComponentActions(formItem, itemLayout);
|
this.addFormItems(components, itemLayout);
|
||||||
});
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,18 +288,33 @@ class FormContainerBuilder extends ContainerBuilderImpl<sqlops.FormContainer, sq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addFormItems(formComponents: Array<sqlops.FormComponent>, itemLayout?: sqlops.FormItemLayout): void {
|
addFormItems(formComponents: Array<sqlops.FormComponent | sqlops.FormComponentGroup>, itemLayout?: sqlops.FormItemLayout): void {
|
||||||
formComponents.forEach(formComponent => {
|
formComponents.forEach(formComponent => {
|
||||||
this.addFormItem(formComponent, itemLayout);
|
this.addFormItem(formComponent, itemLayout);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addFormItem(formComponent: sqlops.FormComponent, itemLayout?: sqlops.FormItemLayout): void {
|
addFormItem(formComponent: sqlops.FormComponent | sqlops.FormComponentGroup, itemLayout?: sqlops.FormItemLayout): void {
|
||||||
|
let componentGroup = formComponent as sqlops.FormComponentGroup;
|
||||||
|
if (componentGroup && componentGroup.components !== undefined) {
|
||||||
|
let labelComponent = this._builder.text().component();
|
||||||
|
labelComponent.value = componentGroup.title;
|
||||||
|
this._component.addItem(labelComponent, { isGroupLabel: true });
|
||||||
|
componentGroup.components.forEach(component => {
|
||||||
|
let layout = component.layout || itemLayout;
|
||||||
|
let itemConfig = this.convertToItemConfig(component, layout);
|
||||||
|
itemConfig.config.isInGroup = true;
|
||||||
|
this._component.addItem(component.component as ComponentWrapper, itemConfig.config);
|
||||||
|
this.addComponentActions(component, layout);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
formComponent = formComponent as sqlops.FormComponent;
|
||||||
let itemImpl = this.convertToItemConfig(formComponent, itemLayout);
|
let itemImpl = this.convertToItemConfig(formComponent, itemLayout);
|
||||||
this._component.addItem(formComponent.component as ComponentWrapper, itemImpl.config);
|
this._component.addItem(formComponent.component as ComponentWrapper, itemImpl.config);
|
||||||
this.addComponentActions(formComponent, itemLayout);
|
this.addComponentActions(formComponent, itemLayout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ToolbarContainerBuilder extends ContainerBuilderImpl<sqlops.ToolbarContainer, any, any> implements sqlops.ToolbarBuilder {
|
class ToolbarContainerBuilder extends ContainerBuilderImpl<sqlops.ToolbarContainer, any, any> implements sqlops.ToolbarBuilder {
|
||||||
withToolbarItems(components: sqlops.ToolbarComponent[]): sqlops.ContainerBuilder<sqlops.ToolbarContainer, any, any> {
|
withToolbarItems(components: sqlops.ToolbarComponent[]): sqlops.ContainerBuilder<sqlops.ToolbarContainer, any, any> {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { ExtHostModelView } from 'sql/workbench/api/node/extHostModelView';
|
|||||||
import { MainThreadModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
|
import { MainThreadModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
|
||||||
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
|
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
|
||||||
import { Deferred } from 'sql/base/common/promise';
|
import { Deferred } from 'sql/base/common/promise';
|
||||||
import { IComponentShape, IItemConfig, ComponentEventType, IComponentEventArgs } from 'sql/workbench/api/common/sqlExtHostTypes';
|
import { IComponentShape, IItemConfig, ComponentEventType, IComponentEventArgs, ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||||
|
import { TitledFormItemLayout } from 'sql/parts/modelComponents/formContainer.component';
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -35,7 +36,8 @@ suite('ExtHostModelView Validation Tests', () => {
|
|||||||
$setLayout: (handle: number, componentId: string, layout: any) => undefined,
|
$setLayout: (handle: number, componentId: string, layout: any) => undefined,
|
||||||
$setProperties: (handle: number, componentId: string, properties: { [key: string]: any }) => undefined,
|
$setProperties: (handle: number, componentId: string, properties: { [key: string]: any }) => undefined,
|
||||||
$registerEvent: (handle: number, componentId: string) => undefined,
|
$registerEvent: (handle: number, componentId: string) => undefined,
|
||||||
dispose: () => undefined
|
dispose: () => undefined,
|
||||||
|
$validate: (handle: number, componentId: string) => undefined
|
||||||
}, MockBehavior.Loose);
|
}, MockBehavior.Loose);
|
||||||
let mainContext = <IMainContext>{
|
let mainContext = <IMainContext>{
|
||||||
getProxy: proxyType => mockProxy.object
|
getProxy: proxyType => mockProxy.object
|
||||||
@@ -130,6 +132,8 @@ suite('ExtHostModelView Validation Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Setting a form component as required initializes the model with the component required', () => {
|
test('Setting a form component as required initializes the model with the component required', () => {
|
||||||
|
mockProxy.setup(x => x.$addToContainer(It.isAny(), It.isAny(), It.isAny())).returns(() => Promise.resolve());
|
||||||
|
|
||||||
// Set up the input component with required initially set to false
|
// Set up the input component with required initially set to false
|
||||||
let inputComponent = modelView.modelBuilder.inputBox().component();
|
let inputComponent = modelView.modelBuilder.inputBox().component();
|
||||||
inputComponent.required = false;
|
inputComponent.required = false;
|
||||||
@@ -148,4 +152,65 @@ suite('ExtHostModelView Validation Tests', () => {
|
|||||||
return rootComponent.itemConfigs.length === 1 && rootComponent.itemConfigs[0].componentShape.id === inputComponent.id && rootComponent.itemConfigs[0].componentShape.properties['required'] === true;
|
return rootComponent.itemConfigs.length === 1 && rootComponent.itemConfigs[0].componentShape.id === inputComponent.id && rootComponent.itemConfigs[0].componentShape.properties['required'] === true;
|
||||||
})), Times.once());
|
})), Times.once());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Form component groups are handled correctly by adding each item in the group and a label to the form', () => {
|
||||||
|
// Set up the mock proxy to save the component that gets initialized so that it can be verified
|
||||||
|
let rootComponent: IComponentShape;
|
||||||
|
mockProxy.setup(x => x.$initializeModel(It.isAny(), It.isAny())).callback((handle, componentShape) => rootComponent = componentShape);
|
||||||
|
mockProxy.setup(x => x.$addToContainer(It.isAny(), It.isAny(), It.isAny())).returns(() => Promise.resolve());
|
||||||
|
|
||||||
|
// Set up the form with a top level component and a group
|
||||||
|
let topLevelList = modelView.modelBuilder.listBox().component();
|
||||||
|
let groupInput = modelView.modelBuilder.inputBox().component();
|
||||||
|
let groupDropdown = modelView.modelBuilder.dropDown().component();
|
||||||
|
|
||||||
|
let topLevelInputFormComponent: sqlops.FormComponent = { component: topLevelList, title: 'top_level_input' };
|
||||||
|
let groupInputFormComponent: sqlops.FormComponent = { component: groupInput, title: 'group_input' };
|
||||||
|
let groupDropdownFormComponent: sqlops.FormComponent = { component: groupDropdown, title: 'group_dropdown' };
|
||||||
|
|
||||||
|
let groupTitle = 'group_title';
|
||||||
|
|
||||||
|
// Give the group a default layout and add one just for the input component too
|
||||||
|
let defaultLayout: sqlops.FormItemLayout = {
|
||||||
|
horizontal: true
|
||||||
|
};
|
||||||
|
let groupInputLayout: sqlops.FormItemLayout = {
|
||||||
|
horizontal: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// If I build a form that has a group with a default layout where one item in the group has its own layout
|
||||||
|
let formContainer = modelView.modelBuilder.formContainer().withFormItems([
|
||||||
|
topLevelInputFormComponent,
|
||||||
|
{
|
||||||
|
components: [
|
||||||
|
Object.assign(groupInputFormComponent, { layout: groupInputLayout }),
|
||||||
|
groupDropdownFormComponent
|
||||||
|
],
|
||||||
|
title: groupTitle
|
||||||
|
}
|
||||||
|
], defaultLayout).component();
|
||||||
|
modelView.initializeModel(formContainer);
|
||||||
|
|
||||||
|
// Then all the items plus a group label are added and have the correct layouts
|
||||||
|
assert.equal(rootComponent.itemConfigs.length, 4);
|
||||||
|
let listBoxConfig = rootComponent.itemConfigs[0];
|
||||||
|
let groupLabelConfig = rootComponent.itemConfigs[1];
|
||||||
|
let inputBoxConfig = rootComponent.itemConfigs[2];
|
||||||
|
let dropdownConfig = rootComponent.itemConfigs[3];
|
||||||
|
|
||||||
|
// Verify that the correct items were added
|
||||||
|
assert.equal(listBoxConfig.componentShape.type, ModelComponentTypes.ListBox);
|
||||||
|
assert.equal(groupLabelConfig.componentShape.type, ModelComponentTypes.Text);
|
||||||
|
assert.equal(inputBoxConfig.componentShape.type, ModelComponentTypes.InputBox);
|
||||||
|
assert.equal(dropdownConfig.componentShape.type, ModelComponentTypes.DropDown);
|
||||||
|
|
||||||
|
// Verify that the group title was set up correctly
|
||||||
|
assert.equal(groupLabelConfig.componentShape.properties['value'], groupTitle);
|
||||||
|
assert.equal((groupLabelConfig.config as TitledFormItemLayout).isGroupLabel, true);
|
||||||
|
|
||||||
|
// Verify that the components' layouts are correct
|
||||||
|
assert.equal((listBoxConfig.config as sqlops.FormItemLayout).horizontal, defaultLayout.horizontal);
|
||||||
|
assert.equal((inputBoxConfig.config as sqlops.FormItemLayout).horizontal, groupInputLayout.horizontal);
|
||||||
|
assert.equal((dropdownConfig.config as sqlops.FormItemLayout).horizontal, defaultLayout.horizontal);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user