initial PR for table designer feature (#17200)

* wip

* wire up e2e

* hook up styler and add as dataprotocal feature

* designer child component rendering

* table component updates

* styler and selectbox column editor

* fix editor size and dupe component creation issue

* fix checkbox column and add more testing data

* properties pane

* only rerender when needed

* properties pane update

* update commands

* cleanup for PR

* revert unwanted changes

* Adding a few tests for Add SQL binding (#17079)

* initial changes

* add a couple more tests

* LEGO: check in for main to temporary branch. (#17089)

* LEGO: check in for main to temporary branch. (#17091)

Co-authored-by: kburtram <karlb@microsoft.com>

* Adds autorest-based SQL Project generation to SQL Database Projects extension (#17078)

* Initial changes

* checkpoint

* Constructing project with post deployment script

* Correcting to intentionally read from cached list of projects

* Adding activation event, fixing fresh workspace bug

* Convert netcoreTool and autorestHelper to share a helper class for streamed command

* Include npm package version to force update

* test checkpoint

* Unit tests

* Added contextual quickpicks for autorest dialogs

* Adding projectController test

* Added projectController test, some refactoring for testability

* Merge branch 'main' into benjin/autorest

* Fixing 'which' import

* PR feedback

* Fixing tests

* Adding additional information for when project provider tests fail

* Hopefully fixing failing tests (unable to repro locally)

* Adding Generate Project item to workspace menu

* PR feedback

* LEGO: check in for main to temporary branch. (#17097)

* added sql database projects strings (#17100)

* Set kernelAlias in startSession when isValidConnection is truthy (#17102)

* PR follow-up comments (#17113)

* Change recompare message after changing options to modal (#17103)

* Change recompare message to modal

* change options to yes and no

* Remove commented code block in git extension (#17116)

* Remove commented code block in git extension

* Add SQL CARBON EDIT tag

* [Loc] Small change to generatingProjectFailed (#17118)

* Add Null Shortcut and added NULL text for default NULL value. (#17085)

* added test key event

* added null function to tryHandleKeyEvent

* added null formatting

* added working null insert.

* added editDataGridPanel string null support

* Bump nth-check from 2.0.0 to 2.0.1 in /build (#17115)

Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Add excludeFlags to extenson marketplace query (#17121)

* Add excludeFlags to extenson marketplace query

* Remove dead code

* Remove extraneous blank line

* Address code review feedback

* Adding Derived Columns to ADS Flatfile Import (#16795)

* Adding derived column boilerplate

* brandan preliminary frontend changes

* empty commit

* added new param

* updating contracts, dialogue changes

* utils changes, saving timeout attempt

* pushing for aasim

* Cleaning up code and fixing the issue in theory

* changing button, did not solve independent scroll

* Fixing the scroll bar issue

* updating flat file service

* adding override keyword to overrriden method

* improving UI

* pushing changes associated with resolved comments

* localizing strings, editing comments

* all comments resolved

* Fixing a test

* updating import package
Updating azure MFA bug

* Clearing navigation validator
Fixing broken table name change

* fixed prose test

* removing unused code from tests

* Fixed PR comments

* Fixing some PR comments

* WIP

* Fixing transformation code and create derived column dialog styling

* removing unused code

* Adding comment for console log

* fixed table styling

* Adding some aria labels

* Fixed some code cleanup issues

* update import service

Co-authored-by: Aasim Khan <aasimkhan30@gmail.com>
Co-authored-by: bnhoule <t-bhoule@microsoft.com>

* Change keybindings for copying query with Results (#17127)

Co-authored-by: Monica Gupta <mogupt@microsoft.com>

* sql proj - publish to docker improvements  (#17124)

* Add AAD token expiration handling for query runner (#17117)

* Add AAD token refresh for notebook

* move token refresh to query management and remove previous refresh calls

* Add guids to RunAll and RunCell events (#17123)

Add guids to RunAll and RunCell events

* add error banner for failed migration cutover and cancel migration (#17106)

* [Loc] update to sql migration, database projects and import extension strings (#17130)

* Apply optional storage class settings in sql mi create command (#17129)

* Make storage classes optional

* Fix notebook to use storage class options

Co-authored-by: Charmaine Chan <chachan@microsoft.com>

* Add support for adding new setting in local.settings.json in add SQL binding quickpick (#17093)

* be able to add new setting in local.settings.json

* cleanup

* addressing comments

* remove todo comment

* addressing comments

* update some strings to uris

* bump version of sqltoolsservice (#17133)

* mark schema compare tests as unstable (#17140)

* [Loc] Update for arc and sql-database-projects (#17148)

* ML extension vbump (#17143)

* Configure docker image and web smoke tests for ADSWeb. (#17136)

* Adjusts timeout period

* Revert "Adjusts timeout period"

This reverts commit 0f372eae2a4611554093b6c09f1ff6c451132e19.

* Adds firefox as browser option

* Corrects yaml smoke test script

* Resets args array to original values

* Corrects build path

* Resolves ignoring browser option error

* continue even after writing to stderr

* Adjusts smoke test (browser) script

* More adjustments to smoke test script

* Corrects server path

* Uses build variable directly in build path

* Specifies browser type since cannot be ignored error

* Adds browser option

* Updates web build image and corrects smoke test exe command

* Removes commented out task

* Updates dockerfile to support web smoketests

* Removes failOnStderr flag

* Use curl instead of wget in Dockerfile

* Fixed a bug with cancling publish (#17160)

* Save And Close Functionality (#17000)

* save and close

* wip

* working save and close

* cleanup

* pr changes

* pr changes

* fix capitalization

* fix build

* pr fix

* Added dynamic options for SQL MIAA Deployment Wizard and updated checkbox field (#17119)

* Dynamic enablement

* Added new package.json field for dynamic options and corresponding functions and classes.

* Enabled dynamic options non-generalized and changed formatting of checkbox to have label on the left.

* Added setOptions under InputComponentInfo for generalization, comments under checkbox component, and changed Replicas to High Availability to reflect parity in portal.

* fix unit test

Co-authored-by: Candice Ye <canye@microsoft.com>
Co-authored-by: Alan Ren <alanren@microsoft.com>

* LEGO: check in for main to temporary branch. (#17168)

* [Loc] added new arc strings and fix for sql-database-projects lcl file for Japanese (#17171)

* [Loc] added new arc strings and fix for sql-database-projects xlf

* removed newline

* Port of Vscode fix for colors too close (#17146)

* default light colors list.focusHighlightForeground too close to list.activeSelectionBackground. Fixes #127597

* remove activeSelectionBackground from theme-carbon as it conflicts with vscode.

* remove dark carbon background

Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>

* Markdown Horizontal Scrollbar Fix (#17083)

* dynamically change horizontal scrollbar

* working horizontal scrollbar

* created new event to handle both scrollbar and mouse wheel

* only show scrollbar when needed

* LEGO: check in for main to temporary branch. (#17181)

* Bump axios to 0.21.4 (#17161)

* Kusto version bump to 0.5.6 (#17114)

* Bumped Kusto toolsservice version to 125 and bumped version to 0.5.6

* Changed netcoreapp3.1 to net5.0 in Kusto config.json

* AzureMonitor bump ToolService version and extension version. (#17174)

* Bump concat-with-sourcemaps from 1.0.4 to 1.1.0 (#17158)

Bumps [concat-with-sourcemaps](https://github.com/floridoo/concat-with-sourcemaps) from 1.0.4 to 1.1.0.
- [Release notes](https://github.com/floridoo/concat-with-sourcemaps/releases)
- [Commits](https://github.com/floridoo/concat-with-sourcemaps/commits)

---
updated-dependencies:
- dependency-name: concat-with-sourcemaps
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update service downloader to 0.2.3 (#17186)

* Notebook Views grid fixes (#17170)

* update data workspace restart ADS to open workspace message (#17188)

* update message

* update string

* Add back "Remove Project" (#17178)

* remove project working with full paths

* use relative paths

* const

* addressing comments

* Bump github-auth axios to 0.21.4 (#17189)

* LEGO: check in for main to temporary branch. (#17192)

* LEGO: check in for main to temporary branch. (#17190)

Co-authored-by: kburtram <karlb@microsoft.com>

* [Loc] Add a small change to dataworkspace (#17194)

* added bump to sqltoolsservice version (#17195)

* Check if file is dirty before adding sql binding (#17175)

* check if file is dirty before adding sql binding

* Addressing comments

* Add vertical scroll bar to Preview in Split View (#17164)

* reset max height

* add editor height

* set md editor height

* Split up NotebookProvider into separate providers for handling file serialization and cell execution. (#17176)

* fix floating promises

* pr comments

* reuse component definition

* comments

* fix error

Co-authored-by: Kim Santiago <31145923+kisantia@users.noreply.github.com>
Co-authored-by: csigs <csigs@users.noreply.github.com>
Co-authored-by: kburtram <karlb@microsoft.com>
Co-authored-by: Benjin Dubishar <benjin.dubishar@gmail.com>
Co-authored-by: Alex Ma <alma1@microsoft.com>
Co-authored-by: Justin M <63619224+JustinMDotNet@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: bnhoule <52506119+bnhoule@users.noreply.github.com>
Co-authored-by: Aasim Khan <aasimkhan30@gmail.com>
Co-authored-by: bnhoule <t-bhoule@microsoft.com>
Co-authored-by: Monica Gupta <scorpio90m@gmail.com>
Co-authored-by: Monica Gupta <mogupt@microsoft.com>
Co-authored-by: Leila Lali <llali@microsoft.com>
Co-authored-by: Hai Cao <hacao@microsoft.com>
Co-authored-by: Daniel Grajeda <dagrajed@microsoft.com>
Co-authored-by: brian-harris <61598682+brian-harris@users.noreply.github.com>
Co-authored-by: Charmaine Chan <69230572+charmainewkchan@users.noreply.github.com>
Co-authored-by: Charmaine Chan <chachan@microsoft.com>
Co-authored-by: Lewis Sanchez <87730006+lewis-sanchez@users.noreply.github.com>
Co-authored-by: Christopher Suh <chsuh@microsoft.com>
Co-authored-by: Candice Ye <candiceye@berkeley.edu>
Co-authored-by: Candice Ye <canye@microsoft.com>
Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
Co-authored-by: Vasu Bhog <vabhog@microsoft.com>
Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
Co-authored-by: Barbara Valdez <34872381+barbaravaldez@users.noreply.github.com>
Co-authored-by: Cory Rivera <corivera@microsoft.com>
This commit is contained in:
Alan Ren
2021-10-08 19:24:31 -07:00
committed by GitHub
parent 8b1fa3ea52
commit abe54a2b92
32 changed files with 2197 additions and 166 deletions

View File

@@ -0,0 +1,480 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerEditIdentifier, DesignerData, DesignerDataPropertyInfo, DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties, DesignerComponentTypeName } from 'sql/base/browser/ui/designer/interfaces';
import { IPanelTab, ITabbedPanelStyles, TabbedPanel } from 'sql/base/browser/ui/panel/panel';
import * as DOM from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import 'vs/css!./media/designer';
import { ITableStyles } from 'sql/base/browser/ui/table/interfaces';
import { IThemable } from 'vs/base/common/styler';
import { Checkbox, ICheckboxStyles } from 'sql/base/browser/ui/checkbox/checkbox';
import { Table } from 'sql/base/browser/ui/table/table';
import { ISelectBoxStyles, SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { localize } from 'vs/nls';
import { TableCellEditorFactory } from 'sql/base/browser/ui/table/tableCellEditorFactory';
import { CheckBoxColumn } from 'sql/base/browser/ui/table/plugins/checkboxColumn.plugin';
import { DesignerTabPanelView } from 'sql/base/browser/ui/designer/designerTabPanelView';
import { DesignerPropertiesPane, PropertiesPaneObjectContext } from 'sql/base/browser/ui/designer/designerPropertiesPane';
export interface IDesignerStyle {
tabbedPanelStyles?: ITabbedPanelStyles;
inputBoxStyles?: IInputBoxStyles;
tableStyles?: ITableStyles;
selectBoxStyles?: ISelectBoxStyles;
checkboxStyles?: ICheckboxStyles;
}
export type DesignerUIComponent = InputBox | Checkbox | Table<Slick.SlickData> | SelectBox;
export type CreateComponentFunc = (container: HTMLElement, component: DesignerDataPropertyInfo, editIdentifier: DesignerEditIdentifier) => DesignerUIComponent;
export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerData) => void;
export class Designer extends Disposable implements IThemable {
private _horizontalSplitViewContainer: HTMLElement;
private _verticalSplitViewContainer: HTMLElement;
private _tabbedPanelContainer: HTMLElement;
private _editorContainer: HTMLElement;
private _horizontalSplitView: SplitView;
private _verticalSplitView: SplitView;
private _tabbedPanel: TabbedPanel;
private _contentContainer: HTMLElement;
private _topContentContainer: HTMLElement;
private _propertiesPaneContainer: HTMLElement;
private _styles: IDesignerStyle = {};
private _supressEditProcessing: boolean = false;
private _componentMap = new Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>();
private _input: DesignerComponentInput;
private _tableCellEditorFactory: TableCellEditorFactory;
private _propertiesPane: DesignerPropertiesPane;
constructor(private readonly _container: HTMLElement,
private readonly _contextViewProvider: IContextViewProvider) {
super();
this._tableCellEditorFactory = new TableCellEditorFactory(
{
valueGetter: (item, column): string => {
return item[column.field].value;
},
valueSetter: async (context: string, row: number, item: DesignerTableComponentRowData, column: Slick.Column<Slick.SlickData>, value: string): Promise<void> => {
await this.handleEdit({
type: DesignerEditType.Update,
property: {
parentProperty: context,
index: row,
property: column.field
},
value: value
});
},
optionsGetter: (item, column): string[] => {
return item[column.field].options;
},
editorStyler: (component) => {
this.styleComponent(component);
}
}, this._contextViewProvider
);
this._verticalSplitViewContainer = DOM.$('.designer-component');
this._horizontalSplitViewContainer = DOM.$('.container');
this._contentContainer = DOM.$('.content-container');
this._topContentContainer = DOM.$('.top-content-container.components-grid');
this._tabbedPanelContainer = DOM.$('.tabbed-panel-container');
this._editorContainer = DOM.$('.editor-container');
this._propertiesPaneContainer = DOM.$('.properties-container');
this._verticalSplitView = new SplitView(this._verticalSplitViewContainer, { orientation: Orientation.VERTICAL });
this._horizontalSplitView = new SplitView(this._horizontalSplitViewContainer, { orientation: Orientation.HORIZONTAL });
this._tabbedPanel = new TabbedPanel(this._tabbedPanelContainer);
this._container.appendChild(this._verticalSplitViewContainer);
this._contentContainer.appendChild(this._topContentContainer);
this._contentContainer.appendChild(this._tabbedPanelContainer);
this._verticalSplitView.addView({
element: this._horizontalSplitViewContainer,
layout: size => {
this.layoutTabbedPanel();
},
minimumSize: 200,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._verticalSplitView.addView({
element: this._editorContainer,
layout: size => { },
minimumSize: 100,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._horizontalSplitView.addView({
element: this._contentContainer,
layout: size => {
this.layoutTabbedPanel();
},
minimumSize: 200,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._horizontalSplitView.addView({
element: this._propertiesPaneContainer,
layout: size => { },
minimumSize: 200,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._propertiesPane = new DesignerPropertiesPane(this._propertiesPaneContainer, (container, component, identifier) => {
return this.createComponent(container, component, identifier, false, false);
}, (definition, component, data) => {
this.setComponentValue(definition, component, data);
}, (component) => {
this.styleComponent(component);
});
const editor = DOM.$('div');
editor.innerText = 'script pane placeholder';
this._editorContainer.appendChild(editor);
}
private styleComponent(component: TabbedPanel | InputBox | Checkbox | Table<Slick.SlickData> | SelectBox): void {
if (component instanceof InputBox) {
component.style(this._styles.inputBoxStyles);
} else if (component instanceof Checkbox) {
component.style(this._styles.checkboxStyles);
} else if (component instanceof TabbedPanel) {
component.style(this._styles.tabbedPanelStyles);
} else if (component instanceof Table) {
component.style(this._styles.tableStyles);
} else {
component.style(this._styles.selectBoxStyles);
}
}
public style(styles: IDesignerStyle): void {
this._styles = styles;
this._componentMap.forEach((value, key, map) => {
if (value.component.style) {
this.styleComponent(value.component);
}
});
this._propertiesPane.style();
this._verticalSplitView.style({
separatorBorder: styles.selectBoxStyles.selectBorder
});
this._horizontalSplitView.style({
separatorBorder: styles.selectBoxStyles.selectBorder
});
}
public layout(dimension: DOM.Dimension) {
this._verticalSplitView.layout(dimension.height);
this._horizontalSplitView.layout(dimension.width);
}
public async setInput(input: DesignerComponentInput): Promise<void> {
this._input = input;
await this.initializeDesignerView();
}
private async initializeDesignerView(): Promise<void> {
this._propertiesPane.clear();
DOM.clearNode(this._topContentContainer);
const view = await this._input.getView();
if (view.components) {
view.components.forEach(component => {
this.createComponent(this._topContentContainer, component, component.propertyName, true, true);
});
}
this._tabbedPanel.clearTabs();
view.tabs.forEach(tab => {
this._tabbedPanel.pushTab(this.createTabView(tab));
});
this.layoutTabbedPanel();
await this.updateComponentValues();
}
private layoutTabbedPanel() {
this._tabbedPanel.layout(new DOM.Dimension(this._tabbedPanelContainer.clientWidth, this._tabbedPanelContainer.clientHeight));
}
private async updateComponentValues(): Promise<void> {
const data = await this._input.getData();
// data[ScriptPropertyName] -- todo- set the script editor
this._componentMap.forEach((value) => {
this.setComponentValue(value.defintion, value.component, data);
});
let type: string;
let components: DesignerDataPropertyInfo[];
let inputData: DesignerData;
let context: PropertiesPaneObjectContext;
const currentContext = this._propertiesPane.context;
if (currentContext === 'root' || currentContext === undefined) {
context = 'root';
components = [];
this._componentMap.forEach(value => {
components.push(value.defintion);
});
type = this._input.objectTypeDisplayName;
inputData = data;
} else {
context = currentContext;
const tableData = data[currentContext.parentProperty] as DesignerTableProperties;
const tableProperties = this._componentMap.get(currentContext.parentProperty).defintion.componentProperties as DesignerTableProperties;
inputData = tableData.data[currentContext.index] as DesignerData;
components = tableProperties.itemProperties;
type = tableProperties.objectTypeDisplayName;
}
this._propertiesPane.show({
context: context,
type: type,
components: components,
data: inputData
});
}
private async handleEdit(edit: DesignerEdit): Promise<void> {
if (this._supressEditProcessing) {
return;
}
await this.applyEdit(edit);
const result = await this._input.processEdit(edit);
if (result.isValid) {
this._supressEditProcessing = true;
await this.updateComponentValues();
this._supressEditProcessing = false;
} else {
//TODO: add error notification
}
}
private async applyEdit(edit: DesignerEdit): Promise<void> {
const data = await this._input.getData();
switch (edit.type) {
case DesignerEditType.Update:
if (typeof edit.property === 'string') {
// if the type of the property is string then the property is a top level property
const componentData = data[edit.property];
const componentType = this._componentMap.get(edit.property).defintion.componentType;
this.setComponentData(componentType, componentData, edit.value);
} else {
const columnPropertyName = edit.property.property;
const tableInfo = this._componentMap.get(edit.property.parentProperty).defintion.componentProperties as DesignerTableProperties;
const tableProperties = data[edit.property.parentProperty] as DesignerTableProperties;
const componentData = tableProperties.data[edit.property.index][columnPropertyName];
const itemProperty = tableInfo.itemProperties.find(property => property.propertyName === columnPropertyName);
if (itemProperty) {
this.setComponentData(itemProperty.componentType, componentData, edit.value);
}
}
break;
default:
break;
}
}
private setComponentData(componentType: DesignerComponentTypeName, componentData: any, value: any): void {
switch (componentType) {
case 'checkbox':
(<CheckBoxProperties>componentData).checked = value;
break;
case 'dropdown':
(<DropDownProperties>componentData).value = value;
break;
case 'input':
(<InputBoxProperties>componentData).value = value;
break;
}
}
private createTabView(tab: DesignerTab): IPanelTab {
const view = new DesignerTabPanelView(tab, (container, component, identifier) => {
return this.createComponent(container, component, identifier, true, false);
});
return {
identifier: tab.title,
title: tab.title,
view: view
};
}
private setComponentValue(definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerData): void {
this._supressEditProcessing = true;
switch (definition.componentType) {
case 'input':
const input = component as InputBox;
const inputData = data[definition.propertyName] as InputBoxProperties;
input.setEnabled(inputData.enabled ?? true);
input.value = inputData.value?.toString() ?? '';
break;
case 'table':
const table = component as Table<Slick.SlickData>;
const tableDataView = table.getData() as TableDataView<Slick.SlickData>;
tableDataView.clear();
tableDataView.push((data[definition.propertyName] as DesignerTableProperties).data);
table.rerenderGrid();
break;
case 'checkbox':
const checkbox = component as Checkbox;
const checkboxData = data[definition.propertyName] as CheckBoxProperties;
if (checkboxData.enabled === false) {
checkbox.disable();
} else {
checkbox.enable();
}
checkbox.checked = checkboxData.checked;
break;
case 'dropdown':
const dropdown = component as SelectBox;
const defaultDropdownData = definition.componentProperties as DropDownProperties;
const dropdownData = data[definition.propertyName] as DropDownProperties;
if (dropdownData.enabled === false) {
dropdown.disable();
} else {
dropdown.enable();
}
const options = (dropdownData.values || defaultDropdownData.values || []) as string[];
dropdown.setOptions(options);
const idx = options?.indexOf(dropdownData.value as string);
if (idx > -1) {
dropdown.select(idx);
}
break;
default:
break;
}
this._supressEditProcessing = false;
}
private createComponent(container: HTMLElement, componentDefinition: DesignerDataPropertyInfo, editIdentifier: DesignerEditIdentifier, addToComponentMap: boolean, setWidth: boolean): DesignerUIComponent {
const componentContainerClass = componentDefinition.componentType === 'table' ? '.full-row' : '';
const labelContainer = container.appendChild(DOM.$(componentContainerClass));
labelContainer.appendChild(DOM.$('span.component-label')).innerText = (componentDefinition.componentType === 'checkbox' || componentDefinition.componentProperties?.title === undefined) ? '' : componentDefinition.componentProperties.title;
const componentDiv = container.appendChild(DOM.$(componentContainerClass));
let component: DesignerUIComponent;
switch (componentDefinition.componentType) {
case 'input':
const inputProperties = componentDefinition.componentProperties as InputBoxProperties;
const input = new InputBox(componentDiv, this._contextViewProvider, {
ariaLabel: inputProperties.title,
type: inputProperties.inputType,
});
input.onDidChange(async (newValue) => {
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue });
});
if (setWidth && inputProperties.width !== undefined) {
input.width = inputProperties.width as number;
}
component = input;
break;
case 'dropdown':
const dropdownProperties = componentDefinition.componentProperties as DropDownProperties;
const dropdown = new SelectBox(dropdownProperties.values as string[], undefined, this._contextViewProvider, undefined);
dropdown.render(componentDiv);
dropdown.selectElem.style.height = '25px';
dropdown.onDidSelect(async (e) => {
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: e.selected });
});
component = dropdown;
break;
case 'checkbox':
const checkboxProperties = componentDefinition.componentProperties as CheckBoxProperties;
const checkbox = new Checkbox(componentDiv, {
label: checkboxProperties.title
});
checkbox.onChange(async (newValue) => {
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue });
});
component = checkbox;
break;
case 'table':
const tableProperties = componentDefinition.componentProperties as DesignerTableProperties;
const table = new Table(componentDiv, {
dataProvider: new TableDataView()
}, {
editable: true,
autoEdit: true,
dataItemColumnValueExtractor: (data: any, column: Slick.Column<Slick.SlickData>): string => {
return data[column.field].value;
}
}
);
table.columns = tableProperties.columns.map(propName => {
const propertyDefinition = tableProperties.itemProperties.find(item => item.propertyName === propName);
switch (propertyDefinition.componentType) {
case 'checkbox':
const checkboxColumn = new CheckBoxColumn({
field: propertyDefinition.propertyName,
name: propertyDefinition.componentProperties.title,
width: propertyDefinition.componentProperties.width as number
});
table.registerPlugin(checkboxColumn);
checkboxColumn.onChange(async (e) => {
await this.handleEdit({
type: DesignerEditType.Update,
property: {
parentProperty: componentDefinition.propertyName,
index: e.row,
property: propertyDefinition.propertyName
},
value: e.value
});
});
return checkboxColumn.definition;
case 'dropdown':
const dropdownProperties = propertyDefinition.componentProperties as DropDownProperties;
return {
name: dropdownProperties.title,
field: propertyDefinition.propertyName,
editor: this._tableCellEditorFactory.getSelectBoxEditorClass(componentDefinition.propertyName, dropdownProperties.values as string[]),
width: dropdownProperties.width as number
};
default:
const inputProperties = propertyDefinition.componentProperties as InputBoxProperties;
return {
name: inputProperties.title,
field: propertyDefinition.propertyName,
editor: this._tableCellEditorFactory.getTextEditorClass(componentDefinition.propertyName, inputProperties.inputType),
width: inputProperties.width as number
};
}
});
table.layout(new DOM.Dimension(container.clientWidth, container.clientHeight));
table.grid.onBeforeEditCell.subscribe((e, data): boolean => {
return data.item[data.column.field].enabled !== false;
});
table.grid.onActiveCellChanged.subscribe((e, data) => {
this._propertiesPane.show({
context: {
parentProperty: componentDefinition.propertyName,
index: data.row
},
type: tableProperties.objectTypeDisplayName,
components: tableProperties.itemProperties,
data: table.getData().getItem(data.row)
});
});
component = table;
break;
default:
throw new Error(localize('tableDesigner.unknownComponentType', "The component type: {0} is not supported", componentDefinition.componentType));
}
if (addToComponentMap) {
this._componentMap.set(componentDefinition.propertyName, {
defintion: componentDefinition,
component: component
});
}
this.styleComponent(component);
return component;
}
}

View File

@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CreateComponentFunc, DesignerUIComponent, SetComponentValueFunc } from 'sql/base/browser/ui/designer/designer';
import { DesignerData, DesignerEditIdentifier, DesignerDataPropertyInfo, InputBoxProperties, NameProperty } from 'sql/base/browser/ui/designer/interfaces';
import * as DOM from 'vs/base/browser/dom';
import { equals } from 'vs/base/common/objects';
import { localize } from 'vs/nls';
export type PropertiesPaneObjectContext = 'root' | {
parentProperty: string;
index: number;
};
export interface ObjectInfo {
context: PropertiesPaneObjectContext;
type: string;
components: DesignerDataPropertyInfo[];
data: DesignerData;
}
export class DesignerPropertiesPane {
private _titleElement: HTMLElement;
private _contentElement: HTMLElement;
private _currentContext?: PropertiesPaneObjectContext;
private _componentMap = new Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>();
constructor(container: HTMLElement, private _createComponent: CreateComponentFunc, private _setComponentValue: SetComponentValueFunc, private _styleComponent: (component: DesignerUIComponent) => void) {
const titleContainer = container.appendChild(DOM.$('.title-container'));
this._titleElement = titleContainer.appendChild(DOM.$('div'));
this._contentElement = container.appendChild(DOM.$('.properties-content.components-grid'));
this._titleElement.innerText = localize('tableDesigner.propertiesPaneTitle', "Properties");
}
public get context(): PropertiesPaneObjectContext | undefined {
return this._currentContext;
}
public clear(): void {
this._componentMap.forEach((value) => {
value.component.dispose();
});
this._componentMap.clear();
DOM.clearNode(this._contentElement);
this._currentContext = undefined;
}
public style() {
this._componentMap.forEach((value) => {
this._styleComponent(value.component);
});
}
public show(item: ObjectInfo): void {
if (!equals(item.context, this._currentContext)) {
this.clear();
this._currentContext = item.context;
item.components.forEach((value) => {
// todo: handle table type in properties pane
if (value.componentType !== 'table') {
const editIdentifier: DesignerEditIdentifier = this._currentContext === 'root' ? value.propertyName : {
parentProperty: this._currentContext.parentProperty,
index: this._currentContext.index,
property: value.propertyName
};
const component = this._createComponent(this._contentElement, value, editIdentifier);
this._componentMap.set(value.propertyName, {
component: component,
defintion: value
});
}
});
}
const name = (<InputBoxProperties>item.data[NameProperty])?.value ?? '';
this._titleElement.innerText = localize({
key: 'tableDesigner.propertiesPaneTitleWithContext',
comment: ['{0} is the place holder for object type', '{1} is the place holder for object name']
}, "Properties - {0} {1}", item.type, name);
this._componentMap.forEach((value) => {
this._setComponentValue(value.defintion, value.component, item.data);
});
}
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DesignerTab } from 'sql/base/browser/ui/designer/interfaces';
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
import { Table } from 'sql/base/browser/ui/table/table';
import { Disposable } from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import { CreateComponentFunc } from 'sql/base/browser/ui/designer/designer';
export class DesignerTabPanelView extends Disposable implements IPanelView {
private _componentsContainer: HTMLElement;
private _tables: Table<Slick.SlickData>[] = [];
constructor(private readonly _tab: DesignerTab, private _createComponent: CreateComponentFunc) {
super();
this._componentsContainer = DOM.$('.components-grid');
this._tab.components.forEach(componentDefition => {
const component = this._createComponent(this._componentsContainer, componentDefition, componentDefition.propertyName);
if (componentDefition.componentType === 'table') {
this._tables.push(component as Table<Slick.SlickData>);
}
});
}
render(container: HTMLElement): void {
container.appendChild(this._componentsContainer);
}
layout(dimension: DOM.Dimension): void {
this._tables.forEach(table => {
table.layout(new DOM.Dimension(dimension.width - 10, dimension.height - 20));
});
}
}

View File

@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface DesignerComponentInput {
/**
* Gets the object type display name.
*/
readonly objectTypeDisplayName: string;
/**
* Gets the designer view specification.
*/
getView(): Promise<DesignerView>;
/**
* Gets the data.
*/
getData(): Promise<DesignerData>;
/**
* Process the edit made in the designer.
* @param edit the information about the edit.
*/
processEdit(edit: DesignerEdit): Promise<DesignerEditResult>;
}
export const NameProperty = 'name';
export const ScriptProperty = 'script';
export interface DesignerView {
components?: DesignerDataPropertyInfo[]
tabs: DesignerTab[];
}
export interface DesignerTab {
title: string;
components: DesignerDataPropertyInfo[];
}
export interface DesignerData {
[key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties;
}
export interface DesignerDataPropertyInfo {
propertyName: string;
componentType: DesignerComponentTypeName;
group?: string;
componentProperties?: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties;
}
export type DesignerComponentTypeName = 'input' | 'checkbox' | 'dropdown' | 'table';
export interface ComponentProperties {
title?: string;
ariaLabel?: string;
width?: number | string;
enabled?: boolean;
}
export interface CategoryValue {
displayName: string;
name: string;
}
export interface DropDownProperties extends ComponentProperties {
value?: string | CategoryValue;
values?: string[] | CategoryValue[];
}
export interface CheckBoxProperties extends ComponentProperties {
checked?: boolean;
}
export interface InputBoxProperties extends ComponentProperties {
value?: string;
inputType?: 'text' | 'number';
}
export interface DesignerTableProperties extends ComponentProperties {
/**
* the name of the properties to be displayed, properties not in this list will be accessible in details view.
*/
columns?: string[];
/**
* The display name of the object type
*/
objectTypeDisplayName: string;
/**
* the properties of the table data item
*/
itemProperties?: DesignerDataPropertyInfo[];
data?: DesignerTableComponentRowData[];
}
export interface DesignerTableComponentRowData {
[key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties;
}
export enum DesignerEditType {
Add = 0,
Remove = 1,
Update = 2
}
export interface DesignerEdit {
type: DesignerEditType;
property: DesignerEditIdentifier;
value: any;
}
export type DesignerEditIdentifier = string | { parentProperty: string, index: number, property: string };
export interface DesignerEditResult {
isValid: boolean;
errors?: { message: string, property?: DesignerEditIdentifier }[];
}

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.designer-component, .designer-component .container {
width: 100%;
height: 100%;
}
.designer-component .content-container {
display: flex;
flex-direction: column;
border-right-width: 1px;
border-right-style: solid;
width: 100%;
height: 100%;
overflow: hidden;
}
.designer-component .top-content-container {
flex: 0 0 auto;
}
.designer-component .editor-container {
border-top-width: 1px;
border-top-style: solid;
width: 100%;
height: 100%;
}
.designer-component .tabbed-panel-container {
flex: 1 1 auto;
overflow: hidden;
}
.designer-component .properties-container {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.designer-component .properties-container .title-container {
padding: 5px;
flex: 0 0 auto;
font-weight: bold;
}
.designer-component .properties-container .properties-content {
flex: 1 1 auto;
overflow-y: auto;
}
.designer-component .component-label {
vertical-align: middle;
}
.designer-component .components-grid {
display: grid;
grid-template-columns: max-content auto; /* label, component*/
grid-template-rows: max-content;
grid-gap: 5px;
padding: 5px;
align-content: start;
box-sizing: border-box;
}
.designer-component .components-grid .full-row {
grid-area: span 1 / span 2; /* spans 1 row and 2 columns*/
}
.designer-component .monaco-table .slick-cell.editable {
padding: 0px;
border-width: 0px;
}

View File

@@ -61,7 +61,7 @@ export class InputBox extends vsInputBox {
if (_sqlOptions && _sqlOptions.type === 'textarea') {
this._isTextAreaInput = true;
}
this.required = !!this._sqlOptions.required;
this.required = !!this._sqlOptions?.required;
}
public override style(styles: IInputBoxStyles): void {
@@ -171,4 +171,9 @@ export class InputBox extends vsInputBox {
}
return undefined;
}
public override set width(width: number) {
super.width = width;
this.element.style.width = 'fit-content';
}
}

View File

@@ -248,6 +248,12 @@ export class TabbedPanel extends Disposable {
}
}
public clearTabs(): void {
this._tabMap.forEach((value, key, map) => {
this.removeTab(key);
});
}
public removeTab(tab: PanelTabIdentifier) {
const actualTab = this._tabMap.get(tab);
if (!actualTab) {

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/checkboxColumn.plugin';
import { BaseTableColumnOptions, TableColumn } from 'sql/base/browser/ui/table/plugins/tableColumn';
import { escape } from 'sql/base/common/strings';
import { Emitter } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
export interface CheckBoxCellValue {
enabled?: boolean;
checked: boolean;
}
export interface CheckBoxChangedEventArgs<T extends Slick.SlickData> {
item: T;
row: number;
column: number;
value: boolean;
}
export interface CheckBoxColumnOptions extends BaseTableColumnOptions {
}
export class CheckBoxColumn<T extends Slick.SlickData> implements Slick.Plugin<T>, TableColumn<T> {
private _handler = new Slick.EventHandler();
private _grid!: Slick.Grid<T>;
private _onChange = new Emitter<CheckBoxChangedEventArgs<T>>();
public onChange = this._onChange.event;
constructor(private options: CheckBoxColumnOptions) {
}
public init(grid: Slick.Grid<T>): void {
this._grid = grid;
this._handler.subscribe(grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs<T>) => this.handleClick(args));
this._handler.subscribe(grid.onKeyDown, (e: DOMEvent, args: Slick.OnKeyDownEventArgs<T>) => this.handleKeyboardEvent(e as KeyboardEvent, args));
this._handler.subscribe(grid.onActiveCellChanged, (e: DOMEvent, args: Slick.OnActiveCellChangedEventArgs<T>) => { this.handleActiveCellChanged(args); });
}
public destroy(): void {
this._handler.unsubscribeAll();
}
public get definition(): Slick.Column<T> {
return {
id: this.options.field,
formatter: (row: number, cell: number, value: any, columnDef: Slick.Column<T>, dataContext: T): string => {
const cellValue = dataContext[columnDef.field] as CheckBoxCellValue;
const escapedTitle = escape(columnDef.name ?? '');
const disabledAttribute = cellValue.enabled === false ? 'disabled' : '';
const checkedAttribute = cellValue.checked ? 'checked' : '';
return `<input type="checkbox" tabindex=-1 title="${escapedTitle}" aria-label="${escapedTitle}" ${checkedAttribute} ${disabledAttribute}/>`;
},
field: this.options.field,
name: this.options.name,
resizable: this.options.resizable,
cssClass: 'slick-plugin-checkbox-column'
};
}
private getCheckbox(): HTMLInputElement {
const cellElement = this._grid.getActiveCellNode();
return cellElement.children[0] as HTMLInputElement;
}
private handleActiveCellChanged(args: Slick.OnActiveCellChangedEventArgs<T>): void {
if (this.isCurrentColumn(args.cell)) {
this.getCheckbox().focus();
}
}
private handleClick(args: Slick.OnClickEventArgs<T>): void {
if (this.isCurrentColumn(args.cell)) {
setTimeout(() => {
this.fireOnChangeEvent();
}, 0);
}
}
private handleKeyboardEvent(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs<T>): void {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Space) && this.isCurrentColumn(args.cell)) {
this.fireOnChangeEvent();
}
}
private fireOnChangeEvent(): void {
const cell = this._grid.getActiveCell();
const checked = this.getCheckbox().checked;
const item = this._grid.getDataItem(cell.row);
const cellValue = item[this.options.field] as CheckBoxCellValue;
if (checked !== cellValue.checked) {
this._onChange.fire({
row: cell.row,
column: cell.cell,
value: checked,
item: item
});
}
}
private isCurrentColumn(columnIndex: number): boolean {
return this._grid.getColumns()[columnIndex]?.id === this.definition.id;
}
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.slick-plugin-checkbox-column {
text-align: center;
}
.slick-plugin-checkbox-column > input{
margin: 0px;
vertical-align: middle;
}

View File

@@ -0,0 +1,172 @@
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { getCodeForKeyCode } from 'vs/base/browser/keyboardEvent';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { KeyCode } from 'vs/base/common/keyCodes';
import * as DOM from 'vs/base/browser/dom';
export interface ITableCellEditorOptions {
valueGetter?: (item: Slick.SlickData, column: Slick.Column<Slick.SlickData>) => string,
valueSetter?: (context: any, row: number, item: Slick.SlickData, column: Slick.Column<Slick.SlickData>, value: string) => Promise<void>,
optionsGetter?: (item: Slick.SlickData, column: Slick.Column<Slick.SlickData>) => string[],
editorStyler: (component: InputBox | SelectBox) => void
}
export class TableCellEditorFactory {
private _options: ITableCellEditorOptions;
constructor(options: ITableCellEditorOptions, private _contextViewProvider: IContextViewProvider) {
this._options = {
valueGetter: options.valueGetter ?? function (item, column) {
return item[column.field];
},
valueSetter: options.valueSetter ?? async function (context, row, item, column, value): Promise<void> {
item[column.field] = value;
},
optionsGetter: options.optionsGetter ?? function (item, column) {
return [];
},
editorStyler: options.editorStyler
};
}
public getTextEditorClass(context: any, inputType: 'text' | 'number' = 'text'): any {
const self = this;
class TextEditor {
private _originalValue: string;
private _input: InputBox;
private _keyCaptureList: number[];
constructor(private _args: Slick.Editors.EditorOptions<Slick.SlickData>) {
this.init();
const keycodesToCapture = [KeyCode.Home, KeyCode.End, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow];
this._keyCaptureList = keycodesToCapture.map(keycode => getCodeForKeyCode(keycode));
}
/**
* The text editor should handle these key press events to avoid event bubble up
*/
public get keyCaptureList(): number[] {
return this._keyCaptureList;
}
public init(): void {
this._input = new InputBox(this._args.container, self._contextViewProvider, {
type: inputType
});
self._options.editorStyler(this._input);
this._input.element.style.height = '100%';
this._input.focus();
}
public destroy(): void {
this._input.dispose();
}
public focus(): void {
this._input.focus();
}
public loadValue(item: Slick.SlickData): void {
this._originalValue = self._options.valueGetter(item, this._args.column) ?? '';
this._input.value = this._originalValue;
}
public async applyValue(item: Slick.SlickData, state: string): Promise<void> {
const activeCell = this._args.grid.getActiveCell();
await self._options.valueSetter(context, activeCell.row, item, this._args.column, state);
}
public isValueChanged(): boolean {
return this._input.value !== this._originalValue.toString();
}
public serializeValue(): any {
return this._input.value;
}
public validate(): Slick.ValidateResults {
return {
valid: true,
msg: undefined
};
}
}
return TextEditor;
}
public getSelectBoxEditorClass(context: any, defaultOptions: string[]): any {
const self = this;
class TextEditor {
private _originalValue: string;
private _selectBox: SelectBox;
private _keyCaptureList: number[];
constructor(private _args: Slick.Editors.EditorOptions<Slick.SlickData>) {
this.init();
const keycodesToCapture = [KeyCode.Home, KeyCode.End, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow];
this._keyCaptureList = keycodesToCapture.map(keycode => getCodeForKeyCode(keycode));
}
/**
* The text editor should handle these key press events to avoid event bubble up
*/
public get keyCaptureList(): number[] {
return this._keyCaptureList;
}
public init(): void {
const container = DOM.$('');
this._args.container.appendChild(container);
this._selectBox = new SelectBox([], undefined, self._contextViewProvider);
container.style.height = '100%';
container.style.width = '100%';
this._selectBox.render(container);
this._selectBox.selectElem.style.height = '100%';
self._options.editorStyler(this._selectBox);
this._selectBox.focus();
}
public destroy(): void {
this._selectBox.dispose();
}
public focus(): void {
this._selectBox.focus();
}
public loadValue(item: Slick.SlickData): void {
this._originalValue = self._options.valueGetter(item, this._args.column) ?? '';
const options = self._options.optionsGetter(item, this._args.column) ?? defaultOptions;
const idx = options?.indexOf(this._originalValue);
if (idx > -1) {
this._selectBox.setOptions(options);
this._selectBox.select(idx);
}
}
public async applyValue(item: Slick.SlickData, state: string): Promise<void> {
const activeCell = this._args.grid.getActiveCell();
await self._options.valueSetter(context, activeCell.row, item, this._args.column, state);
}
public isValueChanged(): boolean {
return this._selectBox.value !== this._originalValue.toString();
}
public serializeValue(): any {
return this._selectBox.value;
}
public validate(): Slick.ValidateResults {
return {
valid: true,
msg: undefined
};
}
}
return TextEditor;
}
}