diff --git a/src/sql/parts/modelComponents/formContainer.component.ts b/src/sql/parts/modelComponents/formContainer.component.ts index 303b113a1f..478f72f865 100644 --- a/src/sql/parts/modelComponents/formContainer.component.ts +++ b/src/sql/parts/modelComponents/formContainer.component.ts @@ -22,13 +22,15 @@ import { getContentHeight, getContentWidth, Dimension } from 'vs/base/browser/do export interface TitledFormItemLayout { title: string; actions?: string[]; - isFormComponent: Boolean; + isFormComponent: boolean; horizontal: boolean; componentWidth?: number | string; componentHeight?: number | string; titleFontSize?: number | string; required?: boolean; info?: string; + isInGroup?: boolean; + isGroupLabel?: boolean; } export interface FormLayout { @@ -43,10 +45,16 @@ class FormItem { template: `
+
+
+ + +
+
-
+
{{getItemTitle(item)}}*
@@ -65,7 +73,7 @@ class FormItem {
-
+
{{getItemTitle(item)}}* @@ -190,11 +198,19 @@ export default class FormContainer extends ContainerBase impleme 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; } - private itemHasActions(item: FormItem): Boolean { + private itemHasActions(item: FormItem): boolean { let itemConfig = item.config; return itemConfig && itemConfig.actions !== undefined && itemConfig.actions.length > 0; } diff --git a/src/sql/parts/modelComponents/formLayout.css b/src/sql/parts/modelComponents/formLayout.css index ed5323224c..f6c8626662 100644 --- a/src/sql/parts/modelComponents/formLayout.css +++ b/src/sql/parts/modelComponents/formLayout.css @@ -56,3 +56,12 @@ padding-right: 5px; display: table-cell; } + +.form-group-item .form-item-row, +.form-group-item.form-cell { + padding-left: 30px; +} + +.form-group-label { + padding-bottom: 0px; +} \ No newline at end of file diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index efc8902941..234ffb40ed 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -82,7 +82,7 @@ declare module 'sqlops' { } export interface FormBuilder extends ContainerBuilder { - withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder; + withFormItems(components: (FormComponent | FormComponentGroup)[], itemLayout?: FormItemLayout): FormBuilder; /** * 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 {*} [itemLayout] Optional layout for the child items */ - addFormItems(formComponents: Array, itemLayout?: FormItemLayout): void; + addFormItems(formComponents: Array, itemLayout?: FormItemLayout): void; /** * 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 {*} [itemLayout] Optional layout for this child item */ - addFormItem(formComponent: FormComponent, itemLayout?: FormItemLayout): void; + addFormItem(formComponent: FormComponent | FormComponentGroup, itemLayout?: FormItemLayout): void; } export interface Component { @@ -137,6 +137,21 @@ declare module 'sqlops' { 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 { component: Component; title?: string; diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index a9e5510279..396bbf93a1 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -41,7 +41,7 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { formContainer(): sqlops.FormBuilder { 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); return container; } @@ -249,14 +249,12 @@ class ContainerBuilderImpl ext } class FormContainerBuilder extends ContainerBuilderImpl implements sqlops.FormBuilder { - withFormItems(components: sqlops.FormComponent[], itemLayout?: sqlops.FormItemLayout): sqlops.ContainerBuilder { - this._component.itemConfigs = components.map(item => { - return this.convertToItemConfig(item, itemLayout); - }); + constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string, private _builder: ModelBuilderImpl) { + super(proxy, handle, type, id); + } - components.forEach(formItem => { - this.addComponentActions(formItem, itemLayout); - }); + withFormItems(components: (sqlops.FormComponent | sqlops.FormComponentGroup)[], itemLayout?: sqlops.FormItemLayout): sqlops.FormBuilder { + this.addFormItems(components, itemLayout); return this; } @@ -290,16 +288,31 @@ class FormContainerBuilder extends ContainerBuilderImpl, itemLayout?: sqlops.FormItemLayout): void { + addFormItems(formComponents: Array, itemLayout?: sqlops.FormItemLayout): void { formComponents.forEach(formComponent => { this.addFormItem(formComponent, itemLayout); }); } - addFormItem(formComponent: sqlops.FormComponent, itemLayout?: sqlops.FormItemLayout): void { - let itemImpl = this.convertToItemConfig(formComponent, itemLayout); - this._component.addItem(formComponent.component as ComponentWrapper, itemImpl.config); - this.addComponentActions(formComponent, itemLayout); + 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); + this._component.addItem(formComponent.component as ComponentWrapper, itemImpl.config); + this.addComponentActions(formComponent, itemLayout); + } } } diff --git a/src/sqltest/workbench/api/extHostModelView.test.ts b/src/sqltest/workbench/api/extHostModelView.test.ts index 2080248383..ab8f2b2df6 100644 --- a/src/sqltest/workbench/api/extHostModelView.test.ts +++ b/src/sqltest/workbench/api/extHostModelView.test.ts @@ -10,7 +10,8 @@ import { ExtHostModelView } from 'sql/workbench/api/node/extHostModelView'; import { MainThreadModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; 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'; @@ -35,7 +36,8 @@ suite('ExtHostModelView Validation Tests', () => { $setLayout: (handle: number, componentId: string, layout: any) => undefined, $setProperties: (handle: number, componentId: string, properties: { [key: string]: any }) => undefined, $registerEvent: (handle: number, componentId: string) => undefined, - dispose: () => undefined + dispose: () => undefined, + $validate: (handle: number, componentId: string) => undefined }, MockBehavior.Loose); let mainContext = { 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', () => { + 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 let inputComponent = modelView.modelBuilder.inputBox().component(); 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; })), 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); + }); }); \ No newline at end of file