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
co-authored by dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> kburtram Kim Santiago csigs Benjin Dubishar Alex Ma Justin M bnhoule Aasim Khan bnhoule Monica Gupta Monica Gupta Leila Lali Hai Cao Daniel Grajeda brian-harris Charmaine Chan Charmaine Chan Lewis Sanchez Christopher Suh Candice Ye Candice Ye Martin Aeschlimann Vasu Bhog Charles Gagnon Barbara Valdez Cory Rivera
parent 8b1fa3ea52
commit abe54a2b92
32 changed files with 2197 additions and 166 deletions
@@ -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;
}
}
@@ -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);
});
}
}
@@ -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));
});
}
}
@@ -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 }[];
}
@@ -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;
}
+6 -1
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';
}
}
+6
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) {
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
}