diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index efc83523cd..9fc6f1ebf6 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -126,6 +126,16 @@ "command": "mssql.showLogFile", "category": "MSSQL", "title": "%title.showLogFile%" + }, + { + "command": "mssql.newTable", + "category": "MSSQL", + "title": "%title.newTable%" + }, + { + "command": "mssql.designTable", + "category": "MSSQL", + "title": "%title.designTable%" } ], "outputChannels": [ @@ -427,6 +437,14 @@ { "command": "mssqlCluster.task.openClusterDashboard", "when": "false" + }, + { + "command": "mssql.newTable", + "when": "false" + }, + { + "command": "mssql.designTable", + "when": "false" } ], "objectExplorer/item/context": [ diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index 52781f2ce7..e4b44ab6fd 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -183,5 +183,8 @@ "databasesListProperties.status": "Status", "databasesListProperties.size": "Size (MB)", "databasesListProperties.lastBackup": "Last backup", - "objectsListProperties.name": "Name" + "objectsListProperties.name": "Name", + + "title.newTable": "New Table", + "title.designTable": "Design" } diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 0f2d423173..8448166aa2 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1024,3 +1024,21 @@ export namespace GetSqlMigrationAssessmentItemsRequest { } // ------------------------------- ----------------------------- + +// ------------------------------- < Table Designer > ------------------------------------ + +export interface TableDesignerEditRequestParams { + tableInfo: azdata.designers.TableInfo, + tableChangeInfo: azdata.designers.DesignerEdit, + data: azdata.designers.DesignerData +} + +export namespace GetTableDesignerInfoRequest { + export const type = new RequestType('tabledesigner/gettabledesignerinfo'); +} + +export namespace ProcessTableDesignerEditRequest { + export const type = new RequestType('tabledesigner/processedit'); +} + +// ------------------------------- < Table Designer > ------------------------------------ diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index 194ba7e303..e4a41c6e0e 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -1082,3 +1082,61 @@ export class ProfilerFeature extends SqlOpsFeature { } } + +/** + * Table Designer Feature + * TODO: Move this feature to data protocol client repo once stablized + */ +export class TableDesignerFeature extends SqlOpsFeature { + private static readonly messagesTypes: RPCMessageType[] = [ + contracts.ProcessTableDesignerEditRequest.type, + ]; + constructor(client: SqlOpsDataClient) { + super(client, TableDesignerFeature.messagesTypes); + } + + public fillClientCapabilities(capabilities: ClientCapabilities): void { + } + + public initialize(capabilities: ServerCapabilities): void { + this.register(this.messages, { + id: UUID.generateUuid(), + registerOptions: undefined + }); + } + + protected registerProvider(options: undefined): Disposable { + const client = this._client; + + const getTableDesignerInfo = async (tableInfo: azdata.designers.TableInfo): Promise => { + try { + return client.sendRequest(contracts.GetTableDesignerInfoRequest.type, tableInfo); + } + catch (e) { + client.logFailedRequest(contracts.GetTableDesignerInfoRequest.type, e); + return Promise.reject(e); + } + }; + const processTableEdit = async (tableInfo: azdata.designers.TableInfo, data: azdata.designers.DesignerData, tableChangeInfo: azdata.designers.DesignerEdit): Promise => { + let params: contracts.TableDesignerEditRequestParams = { + tableInfo: tableInfo, + data: data, + tableChangeInfo: tableChangeInfo + }; + try { + return client.sendRequest(contracts.ProcessTableDesignerEditRequest.type, params); + } + catch (e) { + client.logFailedRequest(contracts.ProcessTableDesignerEditRequest.type, e); + return Promise.reject(e); + } + }; + + return azdata.dataprotocol.registerTableDesignerProvider({ + providerId: client.providerId, + getTableDesignerInfo, + processTableEdit + }); + } +} + diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 2b0063c330..4070711ccf 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -31,6 +31,7 @@ import { promises as fs } from 'fs'; import { IconPathHelper } from './iconHelper'; import * as nls from 'vscode-nls'; import { INotebookConvertService } from './notebookConvert/notebookConvertService'; +import { registerTableDesignerCommands } from './tableDesigner/tableDesigner'; const localize = nls.loadMessageBundle(); const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', "This sample code loads the file into a data frame and shows the first 10 results."); @@ -104,6 +105,8 @@ export async function activate(context: vscode.ExtensionContext): Promise { + await azdata.designers.openTableDesigner(sqlProviderName, { + server: context.connectionProfile.serverName, + database: context.connectionProfile.databaseName, + isNewTable: true + }); + })); + + appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.designTable', async (context: azdata.ObjectExplorerContext) => { + await azdata.designers.openTableDesigner(sqlProviderName, { + server: context.connectionProfile.serverName, + database: context.connectionProfile.databaseName, + isNewTable: false, + name: context.nodeInfo.metadata.name, + schema: context.nodeInfo.metadata.schema + }); + })); + +} diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 10b3295f81..b0cde5618e 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -988,4 +988,271 @@ declare module 'azdata' { */ unsupportedVersionMessage?: string; } + + export enum DataProviderType { + TableDesignerProvider = 'TableDesignerProvider' + } + + export namespace dataprotocol { + export function registerTableDesignerProvider(provider: designers.TableDesignerProvider): vscode.Disposable; + } + + export namespace designers { + /** + * Open a table designer window. + * @param providerId The table designer provider Id. + * @param tableInfo The table information. The object will be passed back to the table designer provider as the unique identifier for the table. + */ + export function openTableDesigner(providerId: string, tableInfo: TableInfo): Thenable; + + /** + * Definition for the table designer provider. + */ + export interface TableDesignerProvider extends DataProvider { + /** + * Gets the table designer information for the specified table. + * @param table the table information. + */ + getTableDesignerInfo(table: TableInfo): Thenable; + /** + * + * @param table the table information + * @param data the object contains the state of the table designer + * @param tableChangeInfo the information about the change user made through the UI. + */ + processTableEdit(table: TableInfo, data: DesignerData, tableChangeInfo: DesignerEdit): Thenable; + } + + /** + * The information of the table. + */ + export interface TableInfo { + /** + * The server name. + */ + server: string; + /** + * The database name + */ + database: string; + /** + * The schema name, only required for existing table. + */ + schema?: string; + /** + * The table name, only required for existing table. + */ + name?: string; + /** + * A boolean value indicates whether a new table is being designed. + */ + isNewTable: boolean; + /** + * Extension can store additional information that the provider needs to uniquely identify a table. + */ + [key: string]: any; + } + + /** + * The information to populate the table designer UI. + */ + export interface TableDesignerInfo { + /** + * The view definition. + */ + view: TableDesignerView; + /** + * The data model. + */ + data: DesignerData; + /** + * The supported column types + */ + columnTypes: string[]; + /** + * The list of schemas in the database. + */ + schemas: string[]; + } + + /** + * Name of the common table properties. + * Extensions can use the names to access the designer data. + */ + export enum TableProperty { + Columns = 'columns', + Description = 'description', + Name = 'name', + Schema = 'schema', + Script = 'script' + } + /** + * Name of the common table column properties. + * Extensions can use the names to access the designer data. + */ + export enum TableColumnProperty { + AllowNulls = 'allowNulls', + DefaultValue = 'defaultValue', + Length = 'length', + Name = 'name', + Type = 'type' + } + + /** + * The table designer view definition + */ + export interface TableDesignerView { + /** + * Additional table properties. Common table properties are handled by Azure Data Studio. see {@link TableProperty} + */ + additionalTableProperties?: DesignerDataPropertyInfo[]; + /** + * Additional table column properties.Common table properties are handled by Azure Data Studio. see {@link TableColumnProperty} + */ + addtionalTableColumnProperties?: DesignerDataPropertyInfo[]; + /** + * Additional tabs. + */ + addtionalTabs?: DesignerTab[]; + } + + /** + * The data model object of the designer. + */ + export interface DesignerData { + [key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties; + } + + /** + * The definition of a designer tab + */ + export interface DesignerTab { + /** + * The title of the tab + */ + title: string; + /** + * the components to be displayed in this tab. + */ + components: DesignerDataPropertyInfo[]; + } + + /** + * The definition of the property in the designer. + */ + export interface DesignerDataPropertyInfo { + /** + * The property name + */ + propertyName: string; + /** + * The component type + */ + componentType: DesignerComponentTypeName; + /** + * The group name, properties with the same group name will be displayed under the same group on the UI. + */ + group?: string; + /** + * The properties of the component. + */ + componentProperties: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties; + } + + /** + * The child component types supported by designer. + */ + export type DesignerComponentTypeName = 'input' | 'checkbox' | 'dropdown' | 'table'; + + /** + * The properties for the table component in the designer. + */ + export interface DesignerTableProperties extends ComponentProperties { + /** + * the name of the properties to be displayed, properties not in this list will be accessible in properties pane. + */ + columns?: string[]; + + /** + * The display name of the object type + */ + objectTypeDisplayName: string; + + /** + * the properties of the table data item + */ + itemProperties?: DesignerDataPropertyInfo[]; + + /** + * The data to be displayed. + */ + data?: DesignerTableComponentDataItem[]; + } + + /** + * The data item of the designer's table component. + */ + export interface DesignerTableComponentDataItem { + [key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties; + } + + /** + * Type of the edit originated from the designer UI. + */ + export enum DesignerEditType { + /** + * Add a row to a table + */ + Add = 0, + /** + * Remove a row from a table + */ + Remove = 1, + /** + * Update a property + */ + Update = 2 + } + + /** + * Information of the edit originated from the designer UI. + */ + export interface DesignerEdit { + /** + * The edit type + */ + type: DesignerEditType; + /** + * the property that was edited + */ + property: DesignerEditIdentifier; + /** + * the new value + */ + value: any; + } + + /** + * The identifier of a property. The value is string typed if the property belongs to the root object, otherwise the type of the value is an object. + */ + export type DesignerEditIdentifier = string | { parentProperty: string, index: number, property: string }; + + /** + * The result returned by the table designer provider after handling an edit request. + */ + export interface DesignerEditResult { + /** + * The data model object. + */ + data: DesignerData; + /** + * Whether the current state is valid. + */ + isValid: boolean; + /** + * Error messages of current state, and the property the caused the error. + */ + errors?: { message: string, property?: DesignerEditIdentifier }[]; + } + } } diff --git a/src/sql/base/browser/ui/designer/designer.ts b/src/sql/base/browser/ui/designer/designer.ts new file mode 100644 index 0000000000..40793a2521 --- /dev/null +++ b/src/sql/base/browser/ui/designer/designer.ts @@ -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 | 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(); + 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, value: string): Promise => { + 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 | 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 { + this._input = input; + await this.initializeDesignerView(); + } + + private async initializeDesignerView(): Promise { + 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 { + 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 { + 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 { + 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': + (componentData).checked = value; + break; + case 'dropdown': + (componentData).value = value; + break; + case 'input': + (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; + const tableDataView = table.getData() as TableDataView; + 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): 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; + } +} diff --git a/src/sql/base/browser/ui/designer/designerPropertiesPane.ts b/src/sql/base/browser/ui/designer/designerPropertiesPane.ts new file mode 100644 index 0000000000..53e8560322 --- /dev/null +++ b/src/sql/base/browser/ui/designer/designerPropertiesPane.ts @@ -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(); + + 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 = (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); + }); + } +} diff --git a/src/sql/base/browser/ui/designer/designerTabPanelView.ts b/src/sql/base/browser/ui/designer/designerTabPanelView.ts new file mode 100644 index 0000000000..f675eafaef --- /dev/null +++ b/src/sql/base/browser/ui/designer/designerTabPanelView.ts @@ -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[] = []; + 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); + } + }); + } + + 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)); + }); + } +} diff --git a/src/sql/base/browser/ui/designer/interfaces.ts b/src/sql/base/browser/ui/designer/interfaces.ts new file mode 100644 index 0000000000..b164d4d4a0 --- /dev/null +++ b/src/sql/base/browser/ui/designer/interfaces.ts @@ -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; + + /** + * Gets the data. + */ + getData(): Promise; + + /** + * Process the edit made in the designer. + * @param edit the information about the edit. + */ + processEdit(edit: DesignerEdit): Promise; +} + +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 }[]; +} diff --git a/src/sql/base/browser/ui/designer/media/designer.css b/src/sql/base/browser/ui/designer/media/designer.css new file mode 100644 index 0000000000..7644b7d26b --- /dev/null +++ b/src/sql/base/browser/ui/designer/media/designer.css @@ -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; +} diff --git a/src/sql/base/browser/ui/inputBox/inputBox.ts b/src/sql/base/browser/ui/inputBox/inputBox.ts index 302f53a4a6..e2870ca680 100644 --- a/src/sql/base/browser/ui/inputBox/inputBox.ts +++ b/src/sql/base/browser/ui/inputBox/inputBox.ts @@ -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'; + } } diff --git a/src/sql/base/browser/ui/panel/panel.ts b/src/sql/base/browser/ui/panel/panel.ts index 380e911207..ace5138f56 100644 --- a/src/sql/base/browser/ui/panel/panel.ts +++ b/src/sql/base/browser/ui/panel/panel.ts @@ -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) { diff --git a/src/sql/base/browser/ui/table/plugins/checkboxColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/checkboxColumn.plugin.ts new file mode 100644 index 0000000000..2de3bd4458 --- /dev/null +++ b/src/sql/base/browser/ui/table/plugins/checkboxColumn.plugin.ts @@ -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 { + item: T; + row: number; + column: number; + value: boolean; +} + +export interface CheckBoxColumnOptions extends BaseTableColumnOptions { +} + +export class CheckBoxColumn implements Slick.Plugin, TableColumn { + private _handler = new Slick.EventHandler(); + private _grid!: Slick.Grid; + private _onChange = new Emitter>(); + public onChange = this._onChange.event; + + constructor(private options: CheckBoxColumnOptions) { + + } + + public init(grid: Slick.Grid): void { + this._grid = grid; + this._handler.subscribe(grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs) => this.handleClick(args)); + this._handler.subscribe(grid.onKeyDown, (e: DOMEvent, args: Slick.OnKeyDownEventArgs) => this.handleKeyboardEvent(e as KeyboardEvent, args)); + this._handler.subscribe(grid.onActiveCellChanged, (e: DOMEvent, args: Slick.OnActiveCellChangedEventArgs) => { this.handleActiveCellChanged(args); }); + } + + public destroy(): void { + this._handler.unsubscribeAll(); + } + + public get definition(): Slick.Column { + return { + id: this.options.field, + formatter: (row: number, cell: number, value: any, columnDef: Slick.Column, 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 ``; + }, + 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): void { + if (this.isCurrentColumn(args.cell)) { + this.getCheckbox().focus(); + } + } + + private handleClick(args: Slick.OnClickEventArgs): void { + if (this.isCurrentColumn(args.cell)) { + setTimeout(() => { + this.fireOnChangeEvent(); + }, 0); + } + } + + private handleKeyboardEvent(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs): 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; + } +} diff --git a/src/sql/base/browser/ui/table/plugins/media/checkboxColumn.plugin.css b/src/sql/base/browser/ui/table/plugins/media/checkboxColumn.plugin.css new file mode 100644 index 0000000000..f71d5f88cc --- /dev/null +++ b/src/sql/base/browser/ui/table/plugins/media/checkboxColumn.plugin.css @@ -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; +} diff --git a/src/sql/base/browser/ui/table/tableCellEditorFactory.ts b/src/sql/base/browser/ui/table/tableCellEditorFactory.ts new file mode 100644 index 0000000000..5b3eec8496 --- /dev/null +++ b/src/sql/base/browser/ui/table/tableCellEditorFactory.ts @@ -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) => string, + valueSetter?: (context: any, row: number, item: Slick.SlickData, column: Slick.Column, value: string) => Promise, + optionsGetter?: (item: Slick.SlickData, column: Slick.Column) => 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 { + 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) { + 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 { + 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) { + 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 { + 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; + } +} diff --git a/src/sql/platform/table/browser/tableService.ts b/src/sql/platform/table/browser/tableService.ts index 9512e2268f..6743753ef8 100644 --- a/src/sql/platform/table/browser/tableService.ts +++ b/src/sql/platform/table/browser/tableService.ts @@ -9,11 +9,11 @@ import { RawContextKey, IContextKey, ContextKeyExpr, IContextKeyService } from ' import { DisposableStore, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { createStyleSheet } from 'vs/base/browser/dom'; -import { attachHighPerfTableStyler as attachTableStyler, defaultHighPerfTableStyles } from 'sql/platform/theme/common/styler'; +import { attachHighPerfTableStyler as attachTableStyler, defaultHighPerfTableStyles, IHighPerfTableStyleOverrides } from 'sql/platform/theme/common/styler'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITableDataSource, ITableColumn } from 'sql/base/browser/ui/table/highPerf/table'; -import { IColorMapping, computeStyles } from 'vs/platform/theme/common/styler'; +import { computeStyles } from 'vs/platform/theme/common/styler'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export const ITableService = createDecorator('tableService'); @@ -118,7 +118,7 @@ function toWorkbenchTableOptions(options: ITableOptions): [ITableOptions extends ITableOptions { - readonly overrideStyles?: IColorMapping; + readonly overrideStyles?: IHighPerfTableStyleOverrides; } export class WorkbenchTable extends Table { diff --git a/src/sql/platform/theme/common/styler.ts b/src/sql/platform/theme/common/styler.ts index cc31394c52..78c59e943b 100644 --- a/src/sql/platform/theme/common/styler.ts +++ b/src/sql/platform/theme/common/styler.ts @@ -8,103 +8,113 @@ import * as colors from './colors'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import * as cr from 'vs/platform/theme/common/colorRegistry'; import * as sqlcr from 'sql/platform/theme/common/colorRegistry'; -import { attachStyler, defaultListStyles, IColorMapping, IStyleOverrides } from 'vs/platform/theme/common/styler'; +import { attachStyler, computeStyles, defaultListStyles, IColorMapping, IStyleOverrides } from 'vs/platform/theme/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IThemable } from 'vs/base/common/styler'; -export function attachDropdownStyler(widget: IThemable, themeService: IThemeService, style?: - { - backgroundColor?: cr.ColorIdentifier, - foregroundColor?: cr.ColorIdentifier, - borderColor?: cr.ColorIdentifier, - buttonForeground?: cr.ColorIdentifier, - buttonBackground?: cr.ColorIdentifier, - buttonHoverBackground?: cr.ColorIdentifier, - buttonFocusOutline?: cr.ColorIdentifier - }): IDisposable { - return attachStyler(themeService, { - foregroundColor: (style && style.foregroundColor) || cr.inputForeground, - borderColor: (style && style.borderColor) || cr.inputBorder, - backgroundColor: (style && style.backgroundColor) || cr.editorBackground, - buttonForeground: (style && style.buttonForeground) || cr.buttonForeground, - buttonBackground: (style && style.buttonBackground) || cr.buttonBackground, - buttonHoverBackground: (style && style.buttonHoverBackground) || cr.buttonHoverBackground, - buttonBorder: cr.contrastBorder, - buttonFocusOutline: (style && style.buttonFocusOutline) || colors.buttonFocusOutline - }, widget); +export interface IDropdownStyleOverrides extends IStyleOverrides { + foregroundColor?: cr.ColorIdentifier; + borderColor?: cr.ColorIdentifier; + backgroundColor?: cr.ColorIdentifier; + buttonForeground?: cr.ColorIdentifier; + buttonBackground?: cr.ColorIdentifier; + buttonHoverBackground?: cr.ColorIdentifier; + buttonBorder?: cr.ColorIdentifier; + buttonFocusOutline?: cr.ColorIdentifier; } -export function attachInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: - { - inputBackground?: cr.ColorIdentifier, - inputForeground?: cr.ColorIdentifier, - disabledInputBackground?: cr.ColorIdentifier, - disabledInputForeground?: cr.ColorIdentifier, - inputBorder?: cr.ColorIdentifier, - inputValidationInfoBorder?: cr.ColorIdentifier, - inputValidationInfoBackground?: cr.ColorIdentifier, - inputValidationWarningBorder?: cr.ColorIdentifier, - inputValidationWarningBackground?: cr.ColorIdentifier, - inputValidationErrorBorder?: cr.ColorIdentifier, - inputValidationErrorBackground?: cr.ColorIdentifier - }): IDisposable { - return attachStyler(themeService, { - inputBackground: (style && style.inputBackground) || cr.inputBackground, - inputForeground: (style && style.inputForeground) || cr.inputForeground, - disabledInputBackground: (style && style.disabledInputBackground) || colors.disabledInputBackground, - disabledInputForeground: (style && style.disabledInputForeground) || colors.disabledInputForeground, - inputBorder: (style && style.inputBorder) || cr.inputBorder, - inputValidationInfoBorder: (style && style.inputValidationInfoBorder) || cr.inputValidationInfoBorder, - inputValidationInfoBackground: (style && style.inputValidationInfoBackground) || cr.inputValidationInfoBackground, - inputValidationWarningBorder: (style && style.inputValidationWarningBorder) || cr.inputValidationWarningBorder, - inputValidationWarningBackground: (style && style.inputValidationWarningBackground) || cr.inputValidationWarningBackground, - inputValidationErrorBorder: (style && style.inputValidationErrorBorder) || cr.inputValidationErrorBorder, - inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || cr.inputValidationErrorBackground - }, widget); +export const defaultDropdownStyle: IDropdownStyleOverrides = { + foregroundColor: cr.inputForeground, + borderColor: cr.inputBorder, + backgroundColor: cr.editorBackground, + buttonForeground: cr.buttonForeground, + buttonBackground: cr.buttonBackground, + buttonHoverBackground: cr.buttonHoverBackground, + buttonBorder: cr.contrastBorder, + buttonFocusOutline: colors.buttonFocusOutline +}; + +export function attachDropdownStyler(widget: IThemable, themeService: IThemeService, style?: IDropdownStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultDropdownStyle, ...(style || {}) }, widget); } -export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeService, style?: - { - selectBackground?: cr.ColorIdentifier, - selectListBackground?: cr.ColorIdentifier, - selectForeground?: cr.ColorIdentifier, - selectBorder?: cr.ColorIdentifier, - disabledSelectBackground?: cr.ColorIdentifier, - disabledSelectForeground?: cr.ColorIdentifier, - inputValidationInfoBorder?: cr.ColorIdentifier, - inputValidationInfoBackground?: cr.ColorIdentifier, - inputValidationWarningBorder?: cr.ColorIdentifier, - inputValidationWarningBackground?: cr.ColorIdentifier, - inputValidationErrorBorder?: cr.ColorIdentifier, - inputValidationErrorBackground?: cr.ColorIdentifier, - focusBorder?: cr.ColorIdentifier, - listFocusBackground?: cr.ColorIdentifier, - listFocusForeground?: cr.ColorIdentifier, - listFocusOutline?: cr.ColorIdentifier, - listHoverBackground?: cr.ColorIdentifier, - listHoverForeground?: cr.ColorIdentifier - }): IDisposable { - return attachStyler(themeService, { - selectBackground: (style && style.selectBackground) || cr.selectBackground, - selectListBackground: (style && style.selectListBackground) || cr.selectListBackground, - selectForeground: (style && style.selectForeground) || cr.selectForeground, - selectBorder: (style && style.selectBorder) || cr.selectBorder, - disabledSelectBackground: (style && style.disabledSelectBackground) || colors.disabledInputBackground, - disabledSelectForeground: (style && style.disabledSelectForeground) || colors.disabledInputForeground, - inputValidationInfoBorder: (style && style.inputValidationInfoBorder) || cr.inputValidationInfoBorder, - inputValidationInfoBackground: (style && style.inputValidationInfoBackground) || cr.inputValidationInfoBackground, - inputValidationWarningBorder: (style && style.inputValidationWarningBorder) || cr.inputValidationWarningBorder, - inputValidationWarningBackground: (style && style.inputValidationWarningBackground) || cr.inputValidationWarningBackground, - inputValidationErrorBorder: (style && style.inputValidationErrorBorder) || cr.inputValidationErrorBorder, - inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || cr.inputValidationErrorBackground, - focusBorder: (style && style.focusBorder) || cr.focusBorder, - listFocusBackground: (style && style.listFocusBackground) || cr.listFocusBackground, - listFocusForeground: (style && style.listFocusForeground) || cr.listFocusForeground, - listFocusOutline: (style && style.listFocusOutline) || cr.activeContrastBorder, - listHoverBackground: (style && style.listHoverBackground) || cr.listHoverBackground, - listHoverForeground: (style && style.listHoverForeground) || cr.listHoverForeground, - listHoverOutline: (style && style.listFocusOutline) || cr.activeContrastBorder - }, widget); +export interface IInputBoxStyleOverrides extends IStyleOverrides { + inputBackground?: cr.ColorIdentifier, + inputForeground?: cr.ColorIdentifier, + disabledInputBackground?: cr.ColorIdentifier, + disabledInputForeground?: cr.ColorIdentifier, + inputBorder?: cr.ColorIdentifier, + inputValidationInfoBorder?: cr.ColorIdentifier, + inputValidationInfoBackground?: cr.ColorIdentifier, + inputValidationWarningBorder?: cr.ColorIdentifier, + inputValidationWarningBackground?: cr.ColorIdentifier, + inputValidationErrorBorder?: cr.ColorIdentifier, + inputValidationErrorBackground?: cr.ColorIdentifier +} + +export const defaultInputBoxStyleOverrides: IInputBoxStyleOverrides = { + inputBackground: cr.inputBackground, + inputForeground: cr.inputForeground, + disabledInputBackground: colors.disabledInputBackground, + disabledInputForeground: colors.disabledInputForeground, + inputBorder: cr.inputBorder, + inputValidationInfoBorder: cr.inputValidationInfoBorder, + inputValidationInfoBackground: cr.inputValidationInfoBackground, + inputValidationWarningBorder: cr.inputValidationWarningBorder, + inputValidationWarningBackground: cr.inputValidationWarningBackground, + inputValidationErrorBorder: cr.inputValidationErrorBorder, + inputValidationErrorBackground: cr.inputValidationErrorBackground +}; + +export function attachInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: IInputBoxStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultInputBoxStyleOverrides, ...(style || {}) }, widget); +} + +export interface ISelectBoxStyleOverrides extends IStyleOverrides { + selectBackground?: cr.ColorIdentifier, + selectListBackground?: cr.ColorIdentifier, + selectForeground?: cr.ColorIdentifier, + selectBorder?: cr.ColorIdentifier, + disabledSelectBackground?: cr.ColorIdentifier, + disabledSelectForeground?: cr.ColorIdentifier, + inputValidationInfoBorder?: cr.ColorIdentifier, + inputValidationInfoBackground?: cr.ColorIdentifier, + inputValidationWarningBorder?: cr.ColorIdentifier, + inputValidationWarningBackground?: cr.ColorIdentifier, + inputValidationErrorBorder?: cr.ColorIdentifier, + inputValidationErrorBackground?: cr.ColorIdentifier, + focusBorder?: cr.ColorIdentifier, + listFocusBackground?: cr.ColorIdentifier, + listFocusForeground?: cr.ColorIdentifier, + listFocusOutline?: cr.ColorIdentifier, + listHoverBackground?: cr.ColorIdentifier, + listHoverForeground?: cr.ColorIdentifier +} + +export const defaultSelectBoxStyleOverrides: ISelectBoxStyleOverrides = { + selectBackground: cr.selectBackground, + selectListBackground: cr.selectListBackground, + selectForeground: cr.selectForeground, + selectBorder: cr.selectBorder, + disabledSelectBackground: colors.disabledInputBackground, + disabledSelectForeground: colors.disabledInputForeground, + inputValidationInfoBorder: cr.inputValidationInfoBorder, + inputValidationInfoBackground: cr.inputValidationInfoBackground, + inputValidationWarningBorder: cr.inputValidationWarningBorder, + inputValidationWarningBackground: cr.inputValidationWarningBackground, + inputValidationErrorBorder: cr.inputValidationErrorBorder, + inputValidationErrorBackground: cr.inputValidationErrorBackground, + focusBorder: cr.focusBorder, + listFocusBackground: cr.listFocusBackground, + listFocusForeground: cr.listFocusForeground, + listFocusOutline: cr.activeContrastBorder, + listHoverBackground: cr.listHoverBackground, + listHoverForeground: cr.listHoverForeground, + listHoverOutline: cr.activeContrastBorder +}; + +export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeService, style?: ISelectBoxStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultSelectBoxStyleOverrides, ...(style || {}) }, widget); } export function attachListBoxStyler(widget: IThemable, themeService: IThemeService, style?: @@ -132,7 +142,7 @@ export function attachListBoxStyler(widget: IThemable, themeService: IThemeServi }, widget); } -export function attachTableStyler(widget: IThemable, themeService: IThemeService, style?: { +export interface ITableStyleOverrides extends IStyleOverrides { listFocusBackground?: cr.ColorIdentifier, listFocusForeground?: cr.ColorIdentifier, listActiveSelectionBackground?: cr.ColorIdentifier, @@ -150,31 +160,35 @@ export function attachTableStyler(widget: IThemable, themeService: IThemeService listSelectionOutline?: cr.ColorIdentifier, listHoverOutline?: cr.ColorIdentifier, tableHeaderBackground?: cr.ColorIdentifier, - tableHeaderForeground?: cr.ColorIdentifier -}): IDisposable { - return attachStyler(themeService, { - listFocusBackground: (style && style.listFocusBackground) || cr.listFocusBackground, - listFocusForeground: (style && style.listFocusForeground) || cr.listFocusForeground, - listActiveSelectionBackground: (style && style.listActiveSelectionBackground) || cr.listActiveSelectionBackground, - listActiveSelectionForeground: (style && style.listActiveSelectionForeground) || cr.listActiveSelectionForeground, - listFocusAndSelectionBackground: style && style.listFocusAndSelectionBackground || colors.listFocusAndSelectionBackground, - listFocusAndSelectionForeground: (style && style.listFocusAndSelectionForeground) || cr.listActiveSelectionForeground, - listInactiveFocusBackground: (style && style.listInactiveFocusBackground), - listInactiveSelectionBackground: (style && style.listInactiveSelectionBackground) || cr.listInactiveSelectionBackground, - listInactiveSelectionForeground: (style && style.listInactiveSelectionForeground) || cr.listInactiveSelectionForeground, - listHoverBackground: (style && style.listHoverBackground) || cr.listHoverBackground, - listHoverForeground: (style && style.listHoverForeground) || cr.listHoverForeground, - listDropBackground: (style && style.listDropBackground) || cr.listDropBackground, - listFocusOutline: (style && style.listFocusOutline) || cr.activeContrastBorder, - listSelectionOutline: (style && style.listSelectionOutline) || cr.activeContrastBorder, - listHoverOutline: (style && style.listHoverOutline) || cr.activeContrastBorder, - listInactiveFocusOutline: style && style.listInactiveFocusOutline, - tableHeaderBackground: (style && style.tableHeaderBackground) || colors.tableHeaderBackground, - tableHeaderForeground: (style && style.tableHeaderForeground) || colors.tableHeaderForeground - }, widget); + tableHeaderForeground?: cr.ColorIdentifier, } -export interface ITableStyleOverrides extends IStyleOverrides { +export const defaultTableStyleOverrides: ITableStyleOverrides = { + listFocusBackground: cr.listFocusBackground, + listFocusForeground: cr.listFocusForeground, + listActiveSelectionBackground: cr.listActiveSelectionBackground, + listActiveSelectionForeground: cr.listActiveSelectionForeground, + listFocusAndSelectionBackground: colors.listFocusAndSelectionBackground, + listFocusAndSelectionForeground: cr.listActiveSelectionForeground, + listInactiveFocusBackground: cr.listInactiveFocusBackground, + listInactiveSelectionBackground: cr.listInactiveSelectionBackground, + listInactiveSelectionForeground: cr.listInactiveSelectionForeground, + listHoverBackground: cr.listHoverBackground, + listHoverForeground: cr.listHoverForeground, + listDropBackground: cr.listDropBackground, + listFocusOutline: cr.activeContrastBorder, + listSelectionOutline: cr.activeContrastBorder, + listHoverOutline: cr.activeContrastBorder, + listInactiveFocusOutline: cr.listInactiveFocusOutline, + tableHeaderBackground: colors.tableHeaderBackground, + tableHeaderForeground: colors.tableHeaderForeground +}; + +export function attachTableStyler(widget: IThemable, themeService: IThemeService, style?: ITableStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultTableStyleOverrides, ...(style || {}) }, widget); +} + +export interface IHighPerfTableStyleOverrides extends IStyleOverrides { listFocusBackground?: cr.ColorIdentifier, listFocusForeground?: cr.ColorIdentifier, listActiveSelectionBackground?: cr.ColorIdentifier, @@ -197,10 +211,6 @@ export interface ITableStyleOverrides extends IStyleOverrides { tableHeaderAndRowCountColor?: cr.ColorIdentifier } -export function attachHighPerfTableStyler(widget: IThemable, themeService: IThemeService, overrides?: IColorMapping): IDisposable { - return attachStyler(themeService, { ...defaultHighPerfTableStyles, ...(overrides || {}) }, widget); -} - export const defaultHighPerfTableStyles: IColorMapping = { listFocusBackground: cr.listFocusBackground, listFocusForeground: cr.listFocusForeground, @@ -223,7 +233,11 @@ export const defaultHighPerfTableStyles: IColorMapping = { tableHeaderAndRowCountColor: colors.tableCellOutline }; -export function attachEditableDropdownStyler(widget: IThemable, themeService: IThemeService, style?: { +export function attachHighPerfTableStyler(widget: IThemable, themeService: IThemeService, overrides?: IHighPerfTableStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultHighPerfTableStyles, ...(overrides || {}) }, widget); +} + +export interface IEditableDropdownStyleOverrides extends IStyleOverrides { listFocusBackground?: cr.ColorIdentifier, listFocusForeground?: cr.ColorIdentifier, listActiveSelectionBackground?: cr.ColorIdentifier, @@ -240,7 +254,6 @@ export function attachEditableDropdownStyler(widget: IThemable, themeService: IT listInactiveFocusOutline?: cr.ColorIdentifier, listSelectionOutline?: cr.ColorIdentifier, listHoverOutline?: cr.ColorIdentifier, - inputBackground?: cr.ColorIdentifier, inputForeground?: cr.ColorIdentifier, inputBorder?: cr.ColorIdentifier, @@ -252,43 +265,53 @@ export function attachEditableDropdownStyler(widget: IThemable, themeService: IT inputValidationErrorBackground?: cr.ColorIdentifier, contextBackground?: cr.ColorIdentifier, contextBorder?: cr.ColorIdentifier -}): IDisposable { - return attachStyler(themeService, { - listFocusBackground: (style && style.listFocusBackground) || cr.listFocusBackground, - listFocusForeground: (style && style.listFocusForeground) || cr.listFocusForeground, - listActiveSelectionBackground: (style && style.listActiveSelectionBackground) || cr.lighten(cr.listActiveSelectionBackground, 0.1), - listActiveSelectionForeground: (style && style.listActiveSelectionForeground) || cr.listActiveSelectionForeground, - listFocusAndSelectionBackground: style && style.listFocusAndSelectionBackground || cr.listActiveSelectionBackground, - listFocusAndSelectionForeground: (style && style.listFocusAndSelectionForeground) || cr.listActiveSelectionForeground, - listInactiveFocusBackground: (style && style.listInactiveFocusBackground), - listInactiveSelectionBackground: (style && style.listInactiveSelectionBackground) || cr.listInactiveSelectionBackground, - listInactiveSelectionForeground: (style && style.listInactiveSelectionForeground) || cr.listInactiveSelectionForeground, - listHoverBackground: (style && style.listHoverBackground) || cr.listHoverBackground, - listHoverForeground: (style && style.listHoverForeground) || cr.listHoverForeground, - listDropBackground: (style && style.listDropBackground) || cr.listDropBackground, - listFocusOutline: (style && style.listFocusOutline) || cr.activeContrastBorder, - listSelectionOutline: (style && style.listSelectionOutline) || cr.activeContrastBorder, - listHoverOutline: (style && style.listHoverOutline) || cr.activeContrastBorder, - listInactiveFocusOutline: style && style.listInactiveFocusOutline, - inputBackground: (style && style.inputBackground) || cr.inputBackground, - inputForeground: (style && style.inputForeground) || cr.inputForeground, - inputBorder: (style && style.inputBorder) || cr.inputBorder, - inputValidationInfoBorder: (style && style.inputValidationInfoBorder) || cr.inputValidationInfoBorder, - inputValidationInfoBackground: (style && style.inputValidationInfoBackground) || cr.inputValidationInfoBackground, - inputValidationWarningBorder: (style && style.inputValidationWarningBorder) || cr.inputValidationWarningBorder, - inputValidationWarningBackground: (style && style.inputValidationWarningBackground) || cr.inputValidationWarningBackground, - inputValidationErrorBorder: (style && style.inputValidationErrorBorder) || cr.inputValidationErrorBorder, - inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || cr.inputValidationErrorBackground, - contextBackground: (style && style.contextBackground) || cr.editorBackground, - contextBorder: (style && style.contextBorder) || cr.inputBorder - }, widget); } -export function attachCheckboxStyler(widget: IThemable, themeService: IThemeService, style?: { disabledCheckboxForeground?: cr.ColorIdentifier }) - : IDisposable { - return attachStyler(themeService, { - disabledCheckboxForeground: (style && style.disabledCheckboxForeground) || colors.disabledCheckboxForeground - }, widget); +export const defaultEditableDropdownStyleOverrides: IEditableDropdownStyleOverrides = { + listFocusBackground: cr.listFocusBackground, + listFocusForeground: cr.listFocusForeground, + listActiveSelectionBackground: cr.listActiveSelectionBackground, + listActiveSelectionForeground: cr.listActiveSelectionForeground, + listFocusAndSelectionBackground: cr.listActiveSelectionBackground, + listFocusAndSelectionForeground: cr.listActiveSelectionForeground, + listInactiveFocusBackground: cr.listInactiveFocusBackground, + listInactiveSelectionBackground: cr.listInactiveSelectionBackground, + listInactiveSelectionForeground: cr.listInactiveSelectionForeground, + listHoverBackground: cr.listHoverBackground, + listHoverForeground: cr.listHoverForeground, + listDropBackground: cr.listDropBackground, + listFocusOutline: cr.activeContrastBorder, + listSelectionOutline: cr.activeContrastBorder, + listHoverOutline: cr.activeContrastBorder, + listInactiveFocusOutline: cr.listInactiveFocusOutline, + inputBackground: cr.inputBackground, + inputForeground: cr.inputForeground, + inputBorder: cr.inputBorder, + inputValidationInfoBorder: cr.inputValidationInfoBorder, + inputValidationInfoBackground: cr.inputValidationInfoBackground, + inputValidationWarningBorder: cr.inputValidationWarningBorder, + inputValidationWarningBackground: cr.inputValidationWarningBackground, + inputValidationErrorBorder: cr.inputValidationErrorBorder, + inputValidationErrorBackground: cr.inputValidationErrorBackground, + contextBackground: cr.editorBackground, + contextBorder: cr.inputBorder +}; + + +export function attachEditableDropdownStyler(widget: IThemable, themeService: IThemeService, style?: IEditableDropdownStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultEditableDropdownStyleOverrides, ...(style || {}) }, widget); +} + +export interface ICheckboxStyleOverrides extends IStyleOverrides { + disabledCheckboxForeground?: cr.ColorIdentifier +} + +export const defaultCheckboxStyleOverrides: ICheckboxStyleOverrides = { + disabledCheckboxForeground: colors.disabledCheckboxForeground +}; + +export function attachCheckboxStyler(widget: IThemable, themeService: IThemeService, style?: ICheckboxStyleOverrides): IDisposable { + return attachStyler(themeService, {}, widget); } export interface IInfoBoxStyleOverrides { @@ -329,9 +352,7 @@ export function attachInfoButtonStyler(widget: IThemable, themeService: IThemeSe export function attachTableFilterStyler(widget: IThemable, themeService: IThemeService): IDisposable { return attachStyler(themeService, { - inputBackground: cr.inputBackground, - inputForeground: cr.inputForeground, - inputBorder: cr.inputBorder, + ...defaultInputBoxStyleOverrides, buttonForeground: cr.buttonForeground, buttonBackground: cr.buttonBackground, buttonHoverBackground: cr.buttonHoverBackground, @@ -349,3 +370,23 @@ export function attachTableFilterStyler(widget: IThemable, themeService: IThemeS ...defaultListStyles, }, widget); } + +export function attachDesignerStyler(widget: any, themeService: IThemeService): IDisposable { + function applyStyles(): void { + const colorTheme = themeService.getColorTheme(); + const inputStyles = computeStyles(colorTheme, defaultInputBoxStyleOverrides); + const selectBoxStyles = computeStyles(colorTheme, defaultSelectBoxStyleOverrides); + const tableStyles = computeStyles(colorTheme, defaultTableStyleOverrides); + const checkboxStyles = computeStyles(colorTheme, defaultCheckboxStyleOverrides); + widget.style({ + inputBoxStyles: inputStyles, + selectBoxStyles: selectBoxStyles, + tableStyles: tableStyles, + checkboxStyles: checkboxStyles + }); + } + + applyStyles(); + + return themeService.onDidColorThemeChange(applyStyles); +} diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index f6a4386cb6..f2b59de010 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -30,6 +30,7 @@ import { IAssessmentService } from 'sql/workbench/services/assessment/common/int import { IDataGridProviderService } from 'sql/workbench/services/dataGridProvider/common/dataGridProviderService'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import { ITableDesignerService } from 'sql/workbench/services/tableDesigner/common/interface'; /** * Main thread class for handling data protocol management registration. @@ -59,7 +60,8 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData @IFileBrowserService private _fileBrowserService: IFileBrowserService, @IAssessmentService private _assessmentService: IAssessmentService, @IDataGridProviderService private _dataGridProviderService: IDataGridProviderService, - @IAdsTelemetryService private _telemetryService: IAdsTelemetryService + @IAdsTelemetryService private _telemetryService: IAdsTelemetryService, + @ITableDesignerService private _tableDesignerService: ITableDesignerService ) { super(); if (extHostContext) { @@ -507,6 +509,20 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData return undefined; } + $registerTableDesignerProvider(providerId: string, handle: number): Promise { + const self = this; + this._tableDesignerService.registerProvider(providerId, { + getTableDesignerInfo(tableInfo: azdata.designers.TableInfo): Thenable { + return self._proxy.$getTableDesignerInfo(handle, tableInfo); + }, + processTableEdit(table, data, edit): Thenable { + return self._proxy.$processTableDesignerEdit(handle, table, data, edit); + } + }); + + return undefined; + } + public $registerSerializationProvider(providerId: string, handle: number): Promise { const self = this; this._serializationService.registerProvider(providerId, { @@ -616,6 +632,11 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData this._jobManagementService.fireOnDidChange(); } + // Table Designer + public $openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo): void { + this._tableDesignerService.openTableDesigner(providerId, tableInfo); + } + public $unregisterProvider(handle: number): Promise { let capabilitiesRegistration = this._capabilitiesRegistrations[handle]; if (capabilitiesRegistration) { diff --git a/src/sql/workbench/api/common/extHostDataProtocol.ts b/src/sql/workbench/api/common/extHostDataProtocol.ts index d63d78a7c7..4fbe6da9c4 100644 --- a/src/sql/workbench/api/common/extHostDataProtocol.ts +++ b/src/sql/workbench/api/common/extHostDataProtocol.ts @@ -194,6 +194,12 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return rt; } + $registerTableDesignerProvider(provider: azdata.designers.TableDesignerProvider): vscode.Disposable { + let rt = this.registerProvider(provider, DataProviderType.TableDesignerProvider); + this._proxy.$registerTableDesignerProvider(provider.providerId, provider.handle); + return rt; + } + // Capabilities Discovery handlers override $getServerCapabilities(handle: number, client: azdata.DataProtocolClientCapabilities): Thenable { return this._resolveProvider(handle).getServerCapabilities(client); @@ -884,4 +890,18 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { public override $getDataGridColumns(handle: number): Thenable { return this._resolveProvider(handle).getDataGridColumns(); } + + // Table Designer + public override $getTableDesignerInfo(handle, table: azdata.designers.TableInfo): Thenable { + return this._resolveProvider(handle).getTableDesignerInfo(table); + } + + public override $processTableDesignerEdit(handle, table: azdata.designers.TableInfo, data: azdata.designers.DesignerData, edit: azdata.designers.DesignerEdit): Thenable { + return this._resolveProvider(handle).processTableEdit(table, data, edit); + } + + public override $openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo): Promise { + this._proxy.$openTableDesigner(providerId, tableInfo); + return Promise.resolve(); + } } diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index 134baedf80..76af9726e1 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -380,6 +380,10 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp return extHostDataProvider.$registerDataGridProvider(provider); }; + let registerTableDesignerProvider = (provider: azdata.designers.TableDesignerProvider): vscode.Disposable => { + return extHostDataProvider.$registerTableDesignerProvider(provider); + }; + // namespace: dataprotocol const dataprotocol: typeof azdata.dataprotocol = { registerBackupProvider, @@ -400,6 +404,7 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp registerSerializationProvider, registerSqlAssessmentServicesProvider, registerDataGridProvider, + registerTableDesignerProvider, onDidChangeLanguageFlavor(listener: (e: azdata.DidChangeLanguageFlavorParams) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) { return extHostDataProvider.onDidChangeLanguageFlavor(listener, thisArgs, disposables); }, @@ -564,6 +569,15 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp SqlAssessmentTargetType: sqlExtHostTypes.SqlAssessmentTargetType }; + const designers: typeof azdata.designers = { + TableProperty: sqlExtHostTypes.designers.TableProperty, + TableColumnProperty: sqlExtHostTypes.designers.TableColumnProperty, + DesignerEditType: sqlExtHostTypes.designers.DesignerEditType, + openTableDesigner(providerId, tableInfo: azdata.designers.TableInfo): Promise { + return extHostDataProvider.$openTableDesigner(providerId, tableInfo); + } + }; + return { version: initData.version, accounts, @@ -614,7 +628,8 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp DatabaseEngineEdition: sqlExtHostTypes.DatabaseEngineEdition, TabOrientation: sqlExtHostTypes.TabOrientation, sqlAssessment, - TextType: sqlExtHostTypes.TextType + TextType: sqlExtHostTypes.TextType, + designers: designers }; } }; diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index af8c877dfa..da1c28b6bb 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -526,6 +526,21 @@ export abstract class ExtHostDataProtocolShape { * Gets the list of columns for a data grid */ $getDataGridColumns(handle: number): Thenable { throw ni(); } + + /** + * Gets the table designer info for the specified table + */ + $getTableDesignerInfo(handle, table: azdata.designers.TableInfo): Thenable { throw ni(); } + + /** + * Process the table edit. + */ + $processTableDesignerEdit(handle, table: azdata.designers.TableInfo, data: azdata.designers.DesignerData, edit: azdata.designers.DesignerEdit): Thenable { throw ni(); } + + /** + * Open a new instance of table designer. + */ + $openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo, designerInfo: azdata.designers.TableDesignerInfo): void { throw ni(); } } /** @@ -591,6 +606,7 @@ export interface MainThreadDataProtocolShape extends IDisposable { $registerSerializationProvider(providerId: string, handle: number): Promise; $registerSqlAssessmentServicesProvider(providerId: string, handle: number): Promise; $registerDataGridProvider(providerId: string, title: string, handle: number): void; + $registerTableDesignerProvider(providerId: string, handle: number): Promise; $unregisterProvider(handle: number): Promise; $onConnectionComplete(handle: number, connectionInfoSummary: azdata.ConnectionInfoSummary): void; $onIntelliSenseCacheComplete(handle: number, connectionUri: string): void; @@ -614,6 +630,7 @@ export interface MainThreadDataProtocolShape extends IDisposable { $onSessionStopped(handle: number, response: azdata.ProfilerSessionStoppedParams): void; $onProfilerSessionCreated(handle: number, response: azdata.ProfilerSessionCreatedParams): void; $onJobDataUpdated(handle: Number): void; + $openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo): void; /** * Callback when a session has completed initialization diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 402b375053..1169a54785 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -381,7 +381,8 @@ export enum DataProviderType { SerializationProvider = 'SerializationProvider', IconProvider = 'IconProvider', SqlAssessmentServicesProvider = 'SqlAssessmentServicesProvider', - DataGridProvider = 'DataGridProvider' + DataGridProvider = 'DataGridProvider', + TableDesignerProvider = 'TableDesignerProvider' } export enum DeclarativeDataType { @@ -905,3 +906,27 @@ export enum TextType { UnorderedList = 'UnorderedList', OrderedList = 'OrderedList' } + +export namespace designers { + export enum TableProperty { + Schema = 'schema', + Name = 'name', + Description = 'description', + Columns = 'columns', + Script = 'script' + } + + export enum TableColumnProperty { + Name = 'name', + Type = 'type', + AllowNulls = 'allowNulls', + DefaultValue = 'defaultValue', + Length = 'length' + } + + export enum DesignerEditType { + Add = 0, + Remove = 1, + Update = 2 + } +} diff --git a/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts b/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts new file mode 100644 index 0000000000..e6b2230578 --- /dev/null +++ b/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { URI } from 'vs/workbench/workbench.web.api'; +import { TableDesignerComponentInput } from 'sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput'; +import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface'; +import * as azdata from 'azdata'; + +const NewTable: string = localize('tableDesigner.newTable', "New Table"); + +export class TableDesignerInput extends EditorInput { + public static ID: string = 'workbench.editorinputs.tableDesignerInput'; + private _designerComponentInput: TableDesignerComponentInput; + constructor(provider: TableDesignerProvider, + private _tableInfo: azdata.designers.TableInfo) { + super(); + this._designerComponentInput = new TableDesignerComponentInput(provider, this._tableInfo); + } + + get typeId(): string { + return TableDesignerInput.ID; + } + + get resource(): URI { + return undefined; + } + + public getComponentInput(): TableDesignerComponentInput { + return this._designerComponentInput; + } + + override getName(): string { + const tableName = this._tableInfo.isNewTable ? NewTable : `${this._tableInfo.schema}.${this._tableInfo.name}`; + return `${this._tableInfo.server}.${this._tableInfo.database} - ${tableName}`; + } +} diff --git a/src/sql/workbench/contrib/tableDesigner/browser/tableDesigner.contribution.ts b/src/sql/workbench/contrib/tableDesigner/browser/tableDesigner.contribution.ts new file mode 100644 index 0000000000..6dea80d063 --- /dev/null +++ b/src/sql/workbench/contrib/tableDesigner/browser/tableDesigner.contribution.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TableDesignerInput } from 'sql/workbench/browser/editor/tableDesigner/tableDesignerInput'; +import { TableDesignerEditor } from 'sql/workbench/contrib/tableDesigner/browser/tableDesignerEditor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions } from 'vs/workbench/common/editor'; + +const tableDesignerDescriptor = EditorDescriptor.create( + TableDesignerEditor, + TableDesignerEditor.ID, + 'TableDesignerEditor' +); + +Registry.as(EditorExtensions.Editors) + .registerEditor(tableDesignerDescriptor, [new SyncDescriptor(TableDesignerInput)]); diff --git a/src/sql/workbench/contrib/tableDesigner/browser/tableDesignerEditor.ts b/src/sql/workbench/contrib/tableDesigner/browser/tableDesignerEditor.ts new file mode 100644 index 0000000000..93e266070e --- /dev/null +++ b/src/sql/workbench/contrib/tableDesigner/browser/tableDesignerEditor.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Designer } from 'sql/base/browser/ui/designer/designer'; +import { attachDesignerStyler } from 'sql/platform/theme/common/styler'; +import { TableDesignerInput } from 'sql/workbench/browser/editor/tableDesigner/tableDesignerInput'; +import * as DOM from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; + +export class TableDesignerEditor extends EditorPane { + public static readonly ID: string = 'workbench.editor.tableDesigner'; + + private _designer: Designer; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IWorkbenchThemeService themeService: IWorkbenchThemeService, + @IStorageService storageService: IStorageService, + @IContextViewService private _contextViewService: IContextViewService + ) { + super(TableDesignerEditor.ID, telemetryService, themeService, storageService); + } + + public override get input(): TableDesignerInput | undefined { + return this._input as TableDesignerInput; + } + + override async setInput(input: TableDesignerInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + this._designer.setInput(input.getComponentInput()); + } + + protected createEditor(parent: HTMLElement): void { + // The editor is only created once per editor group. + this._designer = new Designer(parent, this._contextViewService); + this._register(attachDesignerStyler(this._designer, this.themeService)); + } + + layout(dimension: DOM.Dimension): void { + this._designer.layout(dimension); + } +} diff --git a/src/sql/workbench/services/objectExplorer/common/treeNodeContextKey.ts b/src/sql/workbench/services/objectExplorer/common/treeNodeContextKey.ts index 27b3caab5d..5cc89285b0 100644 --- a/src/sql/workbench/services/objectExplorer/common/treeNodeContextKey.ts +++ b/src/sql/workbench/services/objectExplorer/common/treeNodeContextKey.ts @@ -13,12 +13,14 @@ export class TreeNodeContextKey implements IContextKey { static Status = new RawContextKey('nodeStatus', undefined); static TreeNode = new RawContextKey('treeNode', undefined); static NodeLabel = new RawContextKey('nodeLabel', undefined); + static NodePath = new RawContextKey('nodePath', undefined); private _nodeTypeKey: IContextKey; private _subTypeKey: IContextKey; private _statusKey: IContextKey; private _treeNodeKey: IContextKey; private _nodeLabelKey: IContextKey; + private _nodePathKey: IContextKey; constructor( @IContextKeyService contextKeyService: IContextKeyService @@ -28,6 +30,7 @@ export class TreeNodeContextKey implements IContextKey { this._statusKey = TreeNodeContextKey.Status.bindTo(contextKeyService); this._treeNodeKey = TreeNodeContextKey.TreeNode.bindTo(contextKeyService); this._nodeLabelKey = TreeNodeContextKey.NodeLabel.bindTo(contextKeyService); + this._nodePathKey = TreeNodeContextKey.NodePath.bindTo(contextKeyService); } set(value: TreeNode) { @@ -38,6 +41,7 @@ export class TreeNodeContextKey implements IContextKey { this._statusKey.set(value && value.nodeStatus); } this._nodeLabelKey.set(value && value.label); + this._nodePathKey.set(value && value.nodePath); } reset(): void { @@ -46,6 +50,7 @@ export class TreeNodeContextKey implements IContextKey { this._statusKey.reset(); this._treeNodeKey.reset(); this._nodeLabelKey.reset(); + this._nodePathKey.reset(); } public get(): TreeNode | undefined { diff --git a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts new file mode 100644 index 0000000000..9128235688 --- /dev/null +++ b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { DesignerData, DesignerEdit, DesignerEditResult, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties } from 'sql/base/browser/ui/designer/interfaces'; +import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface'; +import { localize } from 'vs/nls'; +import { designers } from 'sql/workbench/api/common/sqlExtHostTypes'; + +export class TableDesignerComponentInput implements DesignerComponentInput { + + private _data: DesignerData; + private _view: DesignerView; + + constructor(private readonly _provider: TableDesignerProvider, + private _tableInfo: azdata.designers.TableInfo) { + } + + get objectTypeDisplayName(): string { + return localize('tableDesigner.tableObjectType', "Table"); + } + + async getView(): Promise { + if (!this._view) { + await this.initialize(); + } + return this._view; + } + + async getData(): Promise { + if (!this._data) { + await this.initialize(); + } + return this._data; + } + + async processEdit(edit: DesignerEdit): Promise { + const result = await this._provider.processTableEdit(this._tableInfo, this._data!, edit); + if (result.isValid) { + this._data = result.data; + } + return { + isValid: result.isValid, + errors: result.errors + }; + } + + private async initialize(): Promise { + const designerInfo = await this._provider.getTableDesignerInfo(this._tableInfo); + + this._data = designerInfo.data; + this.setDefaultData(); + + const advancedTabComponents: DesignerDataPropertyInfo[] = [ + { + componentType: 'dropdown', + propertyName: designers.TableProperty.Schema, + componentProperties: { + title: localize('tableDesigner.schemaTitle', "Schema"), + values: designerInfo.schemas + } + }, { + componentType: 'input', + propertyName: designers.TableProperty.Description, + componentProperties: { + title: localize('tableDesigner.descriptionTitle', "Description") + } + } + ]; + + if (designerInfo.view.additionalTableProperties) { + advancedTabComponents.push(...designerInfo.view.additionalTableProperties); + } + + const advancedTab = { + title: localize('tableDesigner.advancedTab', "Advanced"), + components: advancedTabComponents + }; + + const columnProperties: DesignerDataPropertyInfo[] = [ + { + componentType: 'input', + propertyName: designers.TableColumnProperty.Name, + componentProperties: { + title: localize('tableDesigner.columnNameTitle', "Name"), + width: 150 + } + }, { + componentType: 'dropdown', + propertyName: designers.TableColumnProperty.Type, + componentProperties: { + title: localize('tableDesigner.columnTypeTitle', "Type"), + width: 100, + values: designerInfo.columnTypes + } + }, { + componentType: 'input', + propertyName: designers.TableColumnProperty.Length, + componentProperties: { + title: localize('tableDesigner.columnLengthTitle', "Length"), + width: 75 + } + }, { + componentType: 'input', + propertyName: designers.TableColumnProperty.DefaultValue, + componentProperties: { + title: localize('tableDesigner.columnDefaultValueTitle', "Default Value"), + width: 150 + } + }, { + componentType: 'checkbox', + propertyName: designers.TableColumnProperty.AllowNulls, + componentProperties: { + title: localize('tableDesigner.columnAllowNullTitle', "Allow Nulls"), + } + } + ]; + + if (designerInfo.view.addtionalTableColumnProperties) { + columnProperties.push(...designerInfo.view.addtionalTableColumnProperties); + } + + const columnsTab = { + title: localize('tableDesigner.columnsTabTitle', "Columns"), + components: [ + { + componentType: 'table', + propertyName: designers.TableProperty.Columns, + componentProperties: { + ariaLabel: localize('tableDesigner.columnsTabTitle', "Columns"), + columns: [ + designers.TableColumnProperty.Name, + designers.TableColumnProperty.Type, + designers.TableColumnProperty.Length, + designers.TableColumnProperty.DefaultValue, + designers.TableColumnProperty.AllowNulls + ], + itemProperties: columnProperties, + objectTypeDisplayName: localize('tableDesigner.columnTypeName', "Column") + } + } + ] + }; + + const tabs = [columnsTab, advancedTab]; + if (designerInfo.view.addtionalTabs) { + tabs.push(...tabs); + } + + this._view = { + components: [{ + componentType: 'input', + propertyName: designers.TableColumnProperty.Name, + componentProperties: { + title: localize('tableDesigner.nameTitle', "Table name"), + width: 200 + } + }], + tabs: tabs + }; + } + + private setDefaultData(): void { + const properties = Object.keys(this._data); + this.setDefaultInputData(properties, designers.TableProperty.Name); + this.setDefaultInputData(properties, designers.TableProperty.Schema); + this.setDefaultInputData(properties, designers.TableProperty.Description); + } + + private setDefaultInputData(allProperties: string[], property: string): void { + if (allProperties.indexOf(property) === -1) { + this._data[property] = {}; + } + } +} diff --git a/src/sql/workbench/services/tableDesigner/browser/tableDesignerService.ts b/src/sql/workbench/services/tableDesigner/browser/tableDesignerService.ts new file mode 100644 index 0000000000..5b760b34be --- /dev/null +++ b/src/sql/workbench/services/tableDesigner/browser/tableDesignerService.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TableDesignerProvider, ITableDesignerService } from 'sql/workbench/services/tableDesigner/common/interface'; +import { invalidProvider } from 'sql/base/common/errors'; +import * as azdata from 'azdata'; +import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { TableDesignerInput } from 'sql/workbench/browser/editor/tableDesigner/tableDesignerInput'; + +export class TableDesignerService implements ITableDesignerService { + + constructor(@IEditorService private _editorService: IEditorService) { } + + public _serviceBrand: undefined; + private _providers = new Map(); + + /** + * Register a data grid provider + */ + public registerProvider(providerId: string, provider: TableDesignerProvider): void { + if (this._providers.has(providerId)) { + throw new Error(`A table designer provider with id "${providerId}" is already registered`); + } + this._providers.set(providerId, provider); + } + + public unregisterProvider(providerId: string): void { + this._providers.delete(providerId); + } + + public getProvider(providerId: string): TableDesignerProvider { + const provider = this._providers.get(providerId); + if (provider) { + return provider; + } + throw invalidProvider(providerId); + } + + public async openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo): Promise { + const provider = this.getProvider(providerId); + const tableDesignerInput = new TableDesignerInput(provider, tableInfo); + await this._editorService.openEditor(tableDesignerInput, { pinned: true }, ACTIVE_GROUP); + } +} diff --git a/src/sql/workbench/services/tableDesigner/common/interface.ts b/src/sql/workbench/services/tableDesigner/common/interface.ts new file mode 100644 index 0000000000..efe40ccb0c --- /dev/null +++ b/src/sql/workbench/services/tableDesigner/common/interface.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + + +export const SERVICE_ID = 'tableDesignerService'; +export const ITableDesignerService = createDecorator(SERVICE_ID); + +export interface TableDesignerProvider extends azdata.designers.TableDesignerProvider { } + +export interface ITableDesignerService { + _serviceBrand: undefined; + + /** + * Register a table designer provider + */ + registerProvider(providerId: string, provider: TableDesignerProvider): void; + + /** + * Unregister a table designer provider + */ + unregisterProvider(providerId: string): void; + + /** + * Gets a registered table designer provider, throwing if none are registered with the specified ID + * @param providerId The id of the registered provider + */ + getProvider(providerId: string): TableDesignerProvider; + + /** + * Open a table designer for the given table + * @param providerId The provider id + * @param tableInfo The table information + */ + openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo): Promise; +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 5a15996474..201c634ecf 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -210,6 +210,8 @@ import { IAssessmentService } from 'sql/workbench/services/assessment/common/int import { AssessmentService } from 'sql/workbench/services/assessment/common/assessmentService'; import { DataGridProviderService } from 'sql/workbench/services/dataGridProvider/browser/dataGridProviderService'; import { IDataGridProviderService } from 'sql/workbench/services/dataGridProvider/common/dataGridProviderService'; +import { ITableDesignerService } from 'sql/workbench/services/tableDesigner/common/interface'; +import { TableDesignerService } from 'sql/workbench/services/tableDesigner/browser/tableDesignerService'; registerSingleton(IDashboardService, DashboardService); registerSingleton(IDashboardViewService, DashboardViewService); @@ -249,6 +251,7 @@ registerSingleton(IObjectExplorerService, ObjectExplorerService); registerSingleton(IOEShimService, OEShimService); registerSingleton(IAssessmentService, AssessmentService); registerSingleton(IDataGridProviderService, DataGridProviderService); +registerSingleton(ITableDesignerService, TableDesignerService); //#endregion @@ -521,4 +524,7 @@ import 'sql/workbench/contrib/azure/browser/azure.contribution'; // Charts import 'sql/workbench/contrib/charts/browser/charts.contribution'; +// table designer +import 'sql/workbench/contrib/tableDesigner/browser/tableDesigner.contribution'; + //#endregion