diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 461b627501..4a2c111974 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "3.0.0-release.207", + "version": "3.0.0-release.211", "downloadFileNames": { "Windows_86": "win-x86-net6.0.zip", "Windows_64": "win-x64-net6.0.zip", diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 66c748d57c..1d32baf097 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1096,7 +1096,7 @@ export namespace InitializeTableDesignerRequest { } export namespace ProcessTableDesignerEditRequest { - export const type = new RequestType('tabledesigner/processedit'); + export const type = new RequestType, void, void>('tabledesigner/processedit'); } export namespace PublishTableDesignerChangesRequest { diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index bbbcf2183f..d3c03131eb 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -1117,7 +1117,7 @@ export class TableDesignerFeature extends SqlOpsFeature { return Promise.reject(e); } }; - const processTableEdit = (tableInfo: azdata.designers.TableInfo, tableChangeInfo: azdata.designers.DesignerEdit): Thenable => { + const processTableEdit = (tableInfo: azdata.designers.TableInfo, tableChangeInfo: azdata.designers.DesignerEdit): Thenable> => { let params: contracts.TableDesignerEditRequestParams = { tableInfo: tableInfo, tableChangeInfo: tableChangeInfo diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index f19729c682..7829525ea1 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -1081,7 +1081,7 @@ declare module 'azdata' { * @param table the table information * @param tableChangeInfo the information about the change user made through the UI. */ - processTableEdit(table: TableInfo, tableChangeInfo: DesignerEdit): Thenable; + processTableEdit(table: TableInfo, tableChangeInfo: DesignerEdit): Thenable>; /** * Publish the changes. @@ -1154,14 +1154,6 @@ declare module 'azdata' { * The initial state of the designer. */ viewModel: DesignerViewModel; - /** - * The supported column types - */ - columnTypes: string[]; - /** - * The list of schemas in the database. - */ - schemas: string[]; } /** @@ -1176,7 +1168,9 @@ declare module 'azdata' { Script = 'script', ForeignKeys = 'foreignKeys', CheckConstraints = 'checkConstraints', - Indexes = 'indexes' + Indexes = 'indexes', + PrimaryKeyName = 'primaryKeyName', + PrimaryKeyColumns = 'primaryKeyColumns' } /** * Name of the common table column properties. @@ -1262,6 +1256,12 @@ declare module 'azdata' { * Default columns to display values are: Name, PrimaryKeyTable. */ foreignKeyTableOptions?: TableDesignerBuiltInTableViewOptions; + /** + * Foreign key column mapping table options. + * Common foreign key column mapping properties are handled by Azure Data Studio. see {@link ForeignKeyColumnMappingProperty}. + * Default columns to display values are: Column, ForeignColumn. + */ + foreignKeyColumnMappingTableOptions?: TableDesignerBuiltInTableViewOptions; /** * Check constraints table options. * Common check constraint properties are handled by Azure Data Studio. see {@link TableCheckConstraintProperty} @@ -1274,13 +1274,22 @@ declare module 'azdata' { * Default columns to display values are: Name. */ indexTableOptions?: TableDesignerBuiltInTableViewOptions; - /** * Index column specification table options. * Common index properties are handled by Azure Data Studio. see {@link TableIndexColumnSpecificationProperty} * Default columns to display values are: Column. */ indexColumnSpecificationTableOptions?: TableDesignerBuiltInTableViewOptions; + /** + * Primary column specification table options. + * Common index properties are handled by Azure Data Studio. see {@link TableIndexColumnSpecificationProperty} + * Default columns to display values are: Column. + */ + primaryKeyColumnSpecificationTableOptions?: TableDesignerBuiltInTableViewOptions; + /** + * Additional primary key properties. Common primary key properties: primaryKeyName. + */ + additionalPrimaryKeyProperties?: DesignerDataPropertyInfo[]; } export interface TableDesignerBuiltInTableViewOptions extends DesignerTablePropertiesBase { @@ -1371,6 +1380,14 @@ declare module 'azdata' { * The confirmation message to be displayed when user removes a row. */ removeRowConfirmationMessage?: string; + /** + * Whether to show the item detail in properties view. The default value is true. + */ + showItemDetailInPropertiesView?: boolean; + /** + * The label of the add new button. The default value is 'Add New'. + */ + labelForAddNewButton?: string; } /** @@ -1399,7 +1416,11 @@ declare module 'azdata' { * The data item of the designer's table component. */ export interface DesignerTableComponentDataItem { - [key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties; + [key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties | boolean; + /** + * Whether the row can be deleted. The default value is true. + */ + canBeDeleted?: boolean; } /** @@ -1458,7 +1479,11 @@ declare module 'azdata' { /** * The result returned by the table designer provider after handling an edit request. */ - export interface DesignerEditResult { + export interface DesignerEditResult { + /** + * The new view information if the view needs to be refreshed. + */ + view?: T; /** * The view model object. */ @@ -1485,6 +1510,10 @@ declare module 'azdata' { * The new view model. */ viewModel: DesignerViewModel; + /** + * The new view. + */ + view: TableDesignerView; } } diff --git a/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts index 2455945361..b570f52084 100644 --- a/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/buttonColumn.plugin'; import 'vs/css!./media/iconColumn'; -import { BaseClickableColumn, getIconCellValue, IconColumnOptions } from 'sql/base/browser/ui/table/plugins/tableColumn'; +import { BaseClickableColumn, ClickableColumnOptions, getIconCellValue, IconColumnOptions } from 'sql/base/browser/ui/table/plugins/tableColumn'; import { escape } from 'sql/base/common/strings'; export interface ButtonCellValue { @@ -13,7 +13,7 @@ export interface ButtonCellValue { title: string; } -export interface ButtonColumnOptions extends IconColumnOptions { +export interface ButtonColumnOptions extends IconColumnOptions, ClickableColumnOptions { /** * Whether to show the text. */ @@ -23,7 +23,7 @@ export interface ButtonColumnOptions extends IconColumnOptions { export class ButtonColumn extends BaseClickableColumn { constructor(private options: ButtonColumnOptions) { - super(); + super(options); } public get definition(): Slick.Column { @@ -39,7 +39,8 @@ export class ButtonColumn extends BaseClickableColumn } const buttonTypeCssClass = this.options.showText ? 'slick-plugin-button slick-plugin-text-button' : 'slick-plugin-button slick-plugin-image-only-button'; const buttonText = this.options.showText ? escapedTitle : ''; - return ``; + const disabledAttribute = this.isCellEnabled(row, cell) ? '' : 'disabled'; + return ``; }, name: this.options.name, resizable: this.options.resizable, diff --git a/src/sql/base/browser/ui/table/plugins/hyperlinkColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/hyperlinkColumn.plugin.ts index 6fbc629e26..4715e4fefc 100644 --- a/src/sql/base/browser/ui/table/plugins/hyperlinkColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/hyperlinkColumn.plugin.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/hyperlinkColumn.plugin'; import 'vs/css!./media/iconColumn'; -import { BaseClickableColumn, getIconCellValue, IconColumnOptions } from 'sql/base/browser/ui/table/plugins/tableColumn'; +import { BaseClickableColumn, getIconCellValue, ClickableColumnOptions, IconColumnOptions } from 'sql/base/browser/ui/table/plugins/tableColumn'; import { escape } from 'sql/base/common/strings'; export interface HyperlinkCellValue { @@ -13,12 +13,12 @@ export interface HyperlinkCellValue { url?: string; } -export interface HyperlinkColumnOptions extends IconColumnOptions { +export interface HyperlinkColumnOptions extends IconColumnOptions, ClickableColumnOptions { } export class HyperlinkColumn extends BaseClickableColumn { constructor(private options: HyperlinkColumnOptions) { - super(); + super(options); } public get definition(): Slick.Column { @@ -31,7 +31,8 @@ export class HyperlinkColumn extends BaseClickableCol const cellValue = dataContext[this.options.field] as HyperlinkCellValue; const cssClasses = iconValue.iconCssClass ? `codicon icon slick-plugin-icon ${iconValue.iconCssClass}` : ''; const urlPart = cellValue?.url ? `href="${encodeURI(cellValue.url)}" target="blank"` : ''; - return `${escapedTitle}`; + const disabledAttribute = this.isCellEnabled(row, cell) ? '' : 'disabled'; + return `${escapedTitle}`; }, name: this.options.name, resizable: true, diff --git a/src/sql/base/browser/ui/table/plugins/media/buttonColumn.plugin.css b/src/sql/base/browser/ui/table/plugins/media/buttonColumn.plugin.css index 8e25e80d3c..25f131a147 100644 --- a/src/sql/base/browser/ui/table/plugins/media/buttonColumn.plugin.css +++ b/src/sql/base/browser/ui/table/plugins/media/buttonColumn.plugin.css @@ -12,6 +12,11 @@ padding-bottom: 0px; } +.slick-plugin-button:disabled { + cursor: not-allowed; + opacity: 0.4; +} + .slick-plugin-button.slick-plugin-text-button { border-width: 1px; width: 100%; @@ -27,4 +32,3 @@ border-width: 0px; background-color: transparent; } - diff --git a/src/sql/base/browser/ui/table/plugins/tableColumn.ts b/src/sql/base/browser/ui/table/plugins/tableColumn.ts index c56097989b..876e5fd8b7 100644 --- a/src/sql/base/browser/ui/table/plugins/tableColumn.ts +++ b/src/sql/base/browser/ui/table/plugins/tableColumn.ts @@ -17,13 +17,20 @@ export interface TableCellClickEventArgs { column: number; } +export interface ClickableColumnOptions { + /** + * The field name of enabled state in the data.The default enabled state is true. + */ + enabledField?: string; +} + export abstract class BaseClickableColumn implements Slick.Plugin, TableColumn { private _handler = new Slick.EventHandler(); private _grid!: Slick.Grid; private _onClick = new Emitter>(); public onClick = this._onClick.event; - constructor() { + constructor(private readonly _options: ClickableColumnOptions) { } public init(grid: Slick.Grid): void { @@ -46,7 +53,7 @@ export abstract class BaseClickableColumn implements public abstract get definition(): Slick.Column; private handleActiveCellChanged(args: Slick.OnActiveCellChangedEventArgs): void { - if (this.isCurrentColumn(args.cell)) { + if (this.isCellEnabled(args.row, args.cell)) { const cellElement = this._grid.getActiveCellNode(); if (cellElement && cellElement.children) { const element = cellElement.children[0] as HTMLElement; @@ -56,7 +63,7 @@ export abstract class BaseClickableColumn implements } private handleClick(args: Slick.OnClickEventArgs): void { - if (this.isCurrentColumn(args.cell)) { + if (this.isCellEnabled(args.row, args.cell)) { // SlickGrid will automatically set active cell on mouse click event, // during the process of setting active cell, blur event will be triggered and handled in a setTimeout block, // on Windows platform, the context menu is html based which will respond the focus related events and hide the context menu. @@ -69,7 +76,7 @@ export abstract class BaseClickableColumn implements private handleKeyboardEvent(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs): void { let event = new StandardKeyboardEvent(e); - if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) && this.isCurrentColumn(args.cell)) { + if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) && this.isCellEnabled(args.row, args.cell)) { event.stopPropagation(); event.preventDefault(); this.fireClickEvent(); @@ -92,8 +99,11 @@ export abstract class BaseClickableColumn implements } } - private isCurrentColumn(columnIndex: number): boolean { - return this._grid.getColumns()[columnIndex]?.id === this.definition.id; + protected isCellEnabled(row: number, cell: number): boolean { + const isCurrentColumn = this._grid.getColumns()[cell]?.id === this.definition.id; + const dataItem = this._grid.getDataItem(row); + const disabled = dataItem && !!this._options.enabledField && dataItem[this._options.enabledField] === false; + return isCurrentColumn && !disabled; } } diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index 8bb5f76c13..74dabd0cfd 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -516,7 +516,7 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData initializeTableDesigner(tableInfo: azdata.designers.TableInfo): Thenable { return self._proxy.$initializeTableDesigner(handle, tableInfo); }, - processTableEdit(table, edit): Thenable { + processTableEdit(table, edit): Thenable> { return self._proxy.$processTableDesignerEdit(handle, table, edit); }, publishChanges(tableInfo: azdata.designers.TableInfo): Thenable { diff --git a/src/sql/workbench/api/common/extHostDataProtocol.ts b/src/sql/workbench/api/common/extHostDataProtocol.ts index 69de4cba78..baca88680a 100644 --- a/src/sql/workbench/api/common/extHostDataProtocol.ts +++ b/src/sql/workbench/api/common/extHostDataProtocol.ts @@ -897,7 +897,7 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return this._resolveProvider(handle).initializeTableDesigner(table); } - public override $processTableDesignerEdit(handle: number, table: azdata.designers.TableInfo, edit: azdata.designers.DesignerEdit): Thenable { + public override $processTableDesignerEdit(handle: number, table: azdata.designers.TableInfo, edit: azdata.designers.DesignerEdit): Thenable> { return this._resolveProvider(handle).processTableEdit(table, edit); } diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 423388f2dd..85b09255b9 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -541,7 +541,7 @@ export abstract class ExtHostDataProtocolShape { /** * Process the table edit. */ - $processTableDesignerEdit(handle: number, table: azdata.designers.TableInfo, edit: azdata.designers.DesignerEdit): Thenable { throw ni(); } + $processTableDesignerEdit(handle: number, table: azdata.designers.TableInfo, edit: azdata.designers.DesignerEdit): Thenable> { throw ni(); } /** * Publish the table designer changes. diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index a00163a5c5..2ae858c55c 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -965,7 +965,9 @@ export namespace designers { Script = 'script', ForeignKeys = 'foreignKeys', CheckConstraints = 'checkConstraints', - Indexes = 'indexes' + Indexes = 'indexes', + PrimaryKeyName = 'primaryKeyName', + PrimaryKeyColumns = 'primaryKeyColumns' } export enum TableColumnProperty { diff --git a/src/sql/workbench/browser/designer/designer.ts b/src/sql/workbench/browser/designer/designer.ts index f0993f6aba..5ec67eebf9 100644 --- a/src/sql/workbench/browser/designer/designer.ts +++ b/src/sql/workbench/browser/designer/designer.ts @@ -6,7 +6,7 @@ import { DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerPropertyPath, DesignerViewModel, DesignerDataPropertyInfo, DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties, - DesignerEditProcessedEventArgs, DesignerStateChangedEventArgs, DesignerAction, DesignerUIState, ScriptProperty, DesignerRootObjectPath + DesignerEditProcessedEventArgs, DesignerStateChangedEventArgs, DesignerAction, ScriptProperty, DesignerRootObjectPath, CanBeDeletedProperty, DesignerUIArea } from 'sql/workbench/browser/designer/interfaces'; import { IPanelTab, ITabbedPanelStyles, TabbedPanel } from 'sql/base/browser/ui/panel/panel'; @@ -42,6 +42,7 @@ import { DesignerPropertyPathValidator } from 'sql/workbench/browser/designer/de import { IThemeService } from 'vs/platform/theme/common/themeService'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { alert } from 'vs/base/browser/ui/aria/aria'; +import { layoutDesignerTable, TableHeaderRowHeight, TableRowHeight } from 'sql/workbench/browser/designer/designerTableUtil'; import { Dropdown, IDropdownStyles } from 'sql/base/browser/ui/editableDropdown/browser/dropdown'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; @@ -62,13 +63,14 @@ export type DesignerUIComponent = InputBox | Checkbox | Table | export type CreateComponentsFunc = (container: HTMLElement, components: DesignerDataPropertyInfo[], parentPath: DesignerPropertyPath) => DesignerUIComponent[]; export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void; -const TableRowHeight = 25; -const TableHeaderRowHeight = 28; +interface DesignerTableCellContext { + view: DesignerUIArea; + path: DesignerPropertyPath; +} + const ScriptTabId = 'scripts'; const MessagesTabId = 'messages'; -type DesignerUIArea = 'PropertiesView' | 'ScriptView' | 'TopContentView' | 'TabsView'; - export class Designer extends Disposable implements IThemable { private _loadingSpinner: LoadingSpinner; private _horizontalSplitViewContainer: HTMLElement; @@ -107,11 +109,12 @@ export class Designer extends Disposable implements IThemable { valueGetter: (item, column): string => { return item[column.field].value; }, - valueSetter: (parentPath: DesignerPropertyPath, row: number, item: DesignerTableComponentRowData, column: Slick.Column, value: string): void => { + valueSetter: (context: DesignerTableCellContext, row: number, item: DesignerTableComponentRowData, column: Slick.Column, value: string): void => { this.handleEdit({ type: DesignerEditType.Update, - path: [...parentPath, row, column.field], - value: value + path: [...context.path, row, column.field], + value: value, + source: context.view }); }, optionsGetter: (item, column): string[] => { @@ -148,7 +151,9 @@ export class Designer extends Disposable implements IThemable { this._scriptTabbedPannel = new TabbedPanel(this._editorContainer); this._messagesView = this._instantiationService.createInstance(DesignerMessagesTabPanelView); this._register(this._messagesView.onMessageSelected((path) => { - this.selectProperty(path); + if (path && path.length > 0) { + this.selectProperty(path); + } })); this._scriptEditorView = new DesignerScriptEditorTabPanelView(this._instantiationService); this._scriptTabbedPannel.pushTab({ @@ -258,24 +263,12 @@ export class Designer extends Disposable implements IThemable { public setInput(input: DesignerComponentInput): void { - // Save state - if (this._input) { - this._input.designerUIState = this.getUIState(); - } - - // Clean up + this.saveUIState(); if (this._loadingTimeoutHandle) { this.stopLoading(); } - this._buttons = []; - this._componentMap.clear(); - DOM.clearNode(this._topContentContainer); - this._contentTabbedPanel.clearTabs(); - this._propertiesPane.clear(); + this.clearUI(); this._inputDisposable?.dispose(); - this._groupHeaders = []; - - // Initialize with new input this._input = input; this._inputDisposable = new DisposableStore(); @@ -307,6 +300,17 @@ export class Designer extends Disposable implements IThemable { this._inputDisposable?.dispose(); } + private clearUI(): void { + this._buttons.forEach(button => button.dispose()); + this._buttons = []; + this._componentMap.forEach(item => item.component.dispose()); + this._componentMap.clear(); + DOM.clearNode(this._topContentContainer); + this._contentTabbedPanel.clearTabs(); + this._propertiesPane.clear(); + this._groupHeaders = []; + } + private initializeDesigner(): void { const view = this._input.view; if (view.components) { @@ -315,8 +319,8 @@ export class Designer extends Disposable implements IThemable { view.tabs.forEach(tab => { this._contentTabbedPanel.pushTab(this.createTabView(tab)); }); - this.layoutTabbedPanel(); this.updateComponentValues(); + this.layoutTabbedPanel(); this.updatePropertiesPane(DesignerRootObjectPath); this.restoreUIState(); } @@ -328,7 +332,15 @@ export class Designer extends Disposable implements IThemable { alert(localize('designer.errorCountAlert', "{0} validation errors found.", args.result.errors.length)); } try { - this.updateComponentValues(); + if (args.result.refreshView) { + this.refresh(); + if (!args.result.isValid) { + this._scriptTabbedPannel.showTab(MessagesTabId); + } + } else { + this.updateComponentValues(); + this.layoutTabbedPanel(); + } if (edit.type === DesignerEditType.Add) { // For tables in the main view, move focus to the first cell of the newly added row, and the properties pane will be showing the new object. if (edit.path.length === 1) { @@ -353,6 +365,10 @@ export class Designer extends Disposable implements IThemable { this.updatePropertiesPane(this._propertiesPane.objectPath); } } + // try to move the focus back to where it was + if (args.result.refreshView) { + this.selectProperty(args.edit.path, args.edit.source, false); + } } catch (err) { this._notificationService.error(err); } @@ -397,8 +413,9 @@ export class Designer extends Disposable implements IThemable { } private refresh() { - this.updateComponentValues(); - this.updatePropertiesPane(this._propertiesPane.objectPath); + this.saveUIState(); + this.clearUI(); + this.initializeDesigner(); } private layoutTabbedPanel() { @@ -408,21 +425,11 @@ export class Designer extends Disposable implements IThemable { private layoutPropertiesPane() { this._propertiesPane?.componentMap.forEach((v) => { if (v.component instanceof Table) { - const rows = v.component.getData().getLength(); - // Tables in properties pane, minimum height:2 rows, maximum height: 10 rows. - const actualHeight = this.getTableHeight(rows); - const minHeight = this.getTableHeight(2); - const maxHeight = this.getTableHeight(10); - const height = Math.min(Math.max(minHeight, actualHeight), maxHeight); - v.component.layout(new DOM.Dimension(this._propertiesPaneContainer.clientWidth - 10 /*padding*/, height)); + layoutDesignerTable(v.component, this._propertiesPaneContainer.clientWidth); } }); } - private getTableHeight(rows: number): number { - return rows * TableRowHeight + TableHeaderRowHeight; - } - private updatePropertiesPane(objectPath: DesignerPropertyPath): void { let type: string; let components: DesignerDataPropertyInfo[]; @@ -485,7 +492,7 @@ export class Designer extends Disposable implements IThemable { this._messagesView.updateMessages(this._input.validationErrors); } - private selectProperty(path: DesignerPropertyPath): void { + private selectProperty(path: DesignerPropertyPath, view?: DesignerUIArea, highlight: boolean = true): void { if (!DesignerPropertyPathValidator.validate(path, this._input.viewModel)) { return; } @@ -505,7 +512,10 @@ export class Designer extends Disposable implements IThemable { if (tab) { for (const component of tab.components) { if (path[0] === component.propertyName) { - this._contentTabbedPanel.showTab(tab.title); + // if we are editing the top level property and the view is properties view, then we don't have to switch to the tab. + if (path.length !== 1 || view !== 'PropertiesView') { + this._contentTabbedPanel.showTab(tab.title); + } found = true; break; } @@ -520,7 +530,12 @@ export class Designer extends Disposable implements IThemable { if (found) { const propertyInfo = this._componentMap.get(path[0]); if (propertyInfo.defintion.componentType !== 'table') { - propertyInfo.component.focus(); + if (view === 'PropertiesView') { + this.updatePropertiesPane(DesignerRootObjectPath); + this._propertiesPane.selectProperty(path); + } else { + propertyInfo.component.focus(); + } return; } else { const tableComponent = >propertyInfo.component; @@ -533,7 +548,9 @@ export class Designer extends Disposable implements IThemable { this._propertiesPane.selectProperty(relativePath); } } - this.highlightActiveElement(); + if (highlight) { + this.highlightActiveElement(); + } } } @@ -703,7 +720,7 @@ export class Designer extends Disposable implements IThemable { }); input.onLoseFocus((args) => { if (args.hasChanged) { - this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: args.value }); + this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: args.value, source: view }); } }); input.onInputFocus(() => { @@ -727,7 +744,7 @@ export class Designer extends Disposable implements IThemable { dropdown = new Dropdown(dropdownContainer, this._contextViewProvider, { values: dropdownProperties.values as string[] || [] }); dropdown.ariaLabel = componentDefinition.componentProperties?.title; dropdown.onValueChange((value) => { - this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: value }); + this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: value, source: view }); }); dropdown.onFocus(() => { if (view === 'PropertiesView') { @@ -742,7 +759,7 @@ export class Designer extends Disposable implements IThemable { dropdown.render(dropdownContainer); dropdown.selectElem.style.height = '25px'; dropdown.onDidSelect((e) => { - this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: e.selected }); + this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: e.selected, source: view }); }); dropdown.onDidFocus(() => { if (view === 'PropertiesView') { @@ -760,7 +777,7 @@ export class Designer extends Disposable implements IThemable { const checkboxProperties = componentDefinition.componentProperties as CheckBoxProperties; const checkbox = new Checkbox(checkboxContainer, { label: '', ariaLabel: checkboxProperties.title }); checkbox.onChange((newValue) => { - this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: newValue }); + this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: newValue, source: view }); }); checkbox.onFocus(() => { if (view === 'PropertiesView') { @@ -778,7 +795,7 @@ export class Designer extends Disposable implements IThemable { const tableProperties = componentDefinition.componentProperties as DesignerTableProperties; if (tableProperties.canAddRows) { const buttonContainer = container.appendChild(DOM.$('.full-row')).appendChild(DOM.$('.add-row-button-container')); - const addNewText = localize('designer.newRowText', "Add New"); + const addNewText = tableProperties.labelForAddNewButton ?? localize('designer.newRowText', "Add New"); const addRowButton = new Button(buttonContainer, { title: addNewText, secondary: true @@ -786,7 +803,8 @@ export class Designer extends Disposable implements IThemable { addRowButton.onDidClick(() => { this.handleEdit({ type: DesignerEditType.Add, - path: propertyPath + path: propertyPath, + source: view }); }); this.styleComponent(addRowButton); @@ -829,7 +847,8 @@ export class Designer extends Disposable implements IThemable { this.handleEdit({ type: DesignerEditType.Update, path: [...propertyPath, e.row, propertyDefinition.propertyName], - value: e.value + value: e.value, + source: view }); }); return checkboxColumn.definition; @@ -838,7 +857,7 @@ export class Designer extends Disposable implements IThemable { return { name: dropdownProperties.title, field: propertyDefinition.propertyName, - editor: this._tableCellEditorFactory.getDropdownEditorClass(propertyPath, dropdownProperties.values as string[], dropdownProperties.isEditable), + editor: this._tableCellEditorFactory.getDropdownEditorClass({ view: view, path: propertyPath }, dropdownProperties.values as string[], dropdownProperties.isEditable), width: dropdownProperties.width as number }; default: @@ -846,7 +865,7 @@ export class Designer extends Disposable implements IThemable { return { name: inputProperties.title, field: propertyDefinition.propertyName, - editor: this._tableCellEditorFactory.getTextEditorClass(propertyPath, inputProperties.inputType), + editor: this._tableCellEditorFactory.getTextEditorClass({ view: view, path: propertyPath }, inputProperties.inputType), width: inputProperties.width as number }; } @@ -858,7 +877,8 @@ export class Designer extends Disposable implements IThemable { title: localize('designer.removeRowText', "Remove"), width: 20, resizable: false, - isFontIcon: true + isFontIcon: true, + enabledField: CanBeDeletedProperty }); deleteRowColumn.onClick(async (e) => { if (tableProperties.showRemoveRowConfirmation) { @@ -873,7 +893,8 @@ export class Designer extends Disposable implements IThemable { } this.handleEdit({ type: DesignerEditType.Remove, - path: [...propertyPath, e.row] + path: [...propertyPath, e.row], + source: view }); }); table.registerPlugin(deleteRowColumn); @@ -887,7 +908,11 @@ export class Designer extends Disposable implements IThemable { table.grid.onActiveCellChanged.subscribe((e, data) => { if (view === 'TabsView' || view === 'TopContentView') { if (data.row !== undefined) { - this.updatePropertiesPane([...propertyPath, data.row]); + if (tableProperties.showItemDetailInPropertiesView === false) { + this.updatePropertiesPane(DesignerRootObjectPath); + } else { + this.updatePropertiesPane([...propertyPath, data.row]); + } } else { this.updatePropertiesPane(DesignerRootObjectPath); } @@ -934,11 +959,13 @@ export class Designer extends Disposable implements IThemable { } } - private getUIState(): DesignerUIState { - return { - activeContentTabId: this._contentTabbedPanel.activeTabId, - activeScriptTabId: this._scriptTabbedPannel.activeTabId - }; + private saveUIState(): void { + if (this._input) { + this._input.designerUIState = { + activeContentTabId: this._contentTabbedPanel.activeTabId, + activeScriptTabId: this._scriptTabbedPannel.activeTabId + }; + } } private restoreUIState(): void { diff --git a/src/sql/workbench/browser/designer/designerPropertyPathValidator.ts b/src/sql/workbench/browser/designer/designerPropertyPathValidator.ts index c2b11779f9..ff46ab62f3 100644 --- a/src/sql/workbench/browser/designer/designerPropertyPathValidator.ts +++ b/src/sql/workbench/browser/designer/designerPropertyPathValidator.ts @@ -56,7 +56,7 @@ export class DesignerPropertyPathValidator { if (!tableData.data || tableData.data.length - 1 < objectIndex) { return false; } - currentObject = tableData.data[objectIndex]; + currentObject = tableData.data[objectIndex] as DesignerViewModel; index++; } return true; diff --git a/src/sql/workbench/browser/designer/designerTabPanelView.ts b/src/sql/workbench/browser/designer/designerTabPanelView.ts index 9934afbf94..aa9b59c531 100644 --- a/src/sql/workbench/browser/designer/designerTabPanelView.ts +++ b/src/sql/workbench/browser/designer/designerTabPanelView.ts @@ -9,17 +9,16 @@ 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 { CreateComponentsFunc } from 'sql/workbench/browser/designer/designer'; - -const ButtonHeight = 30; -const HorizontalPadding = 10; -const VerticalPadding = 20; +import { layoutDesignerTable } from 'sql/workbench/browser/designer/designerTableUtil'; export class DesignerTabPanelView extends Disposable implements IPanelView { + private _viewContainer: HTMLElement; private _componentsContainer: HTMLElement; private _tables: Table[] = []; constructor(private readonly _tab: DesignerTab, private _createComponents: CreateComponentsFunc) { super(); - this._componentsContainer = DOM.$('.components-grid'); + this._viewContainer = DOM.$('.designer-tab-view'); + this._componentsContainer = this._viewContainer.appendChild(DOM.$('.components-grid')); const uiComponents = this._createComponents(this._componentsContainer, this._tab.components, DesignerRootObjectPath); uiComponents.forEach(component => { if (component instanceof Table) { @@ -29,12 +28,12 @@ export class DesignerTabPanelView extends Disposable implements IPanelView { } render(container: HTMLElement): void { - container.appendChild(this._componentsContainer); + container.appendChild(this._viewContainer); } layout(dimension: DOM.Dimension): void { this._tables.forEach(table => { - table.layout(new DOM.Dimension(dimension.width - HorizontalPadding, dimension.height - VerticalPadding - ButtonHeight)); + layoutDesignerTable(table, dimension.width); }); } } diff --git a/src/sql/workbench/browser/designer/designerTableUtil.ts b/src/sql/workbench/browser/designer/designerTableUtil.ts new file mode 100644 index 0000000000..14b7d6fb66 --- /dev/null +++ b/src/sql/workbench/browser/designer/designerTableUtil.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Table } from 'sql/base/browser/ui/table/table'; +import * as DOM from 'vs/base/browser/dom'; +import { deepClone } from 'vs/base/common/objects'; + +export const TableRowHeight = 25; +export const TableHeaderRowHeight = 28; +const minHeight = getTableHeight(2); + +/** + * Layout the table, the height will be determined by the number of rows in it. + * @param table the table. + * @param width width of the table + */ +export function layoutDesignerTable(table: Table, width: number): void { + let activeCell: Slick.Cell = undefined; + if (table.container.contains(document.activeElement)) { + // Note down the current active cell if the focus is currently in the table + // After the table layout operation is done, the focus will be restored. + activeCell = deepClone(table.activeCell); + } + const rows = table.getData().getLength(); + const actualHeight = getTableHeight(rows); + const height = Math.max(minHeight, actualHeight); + table.layout(new DOM.Dimension(width - 20 /* Padding and scroll bar */, height)); + if (activeCell && rows > activeCell.row) { + table.setActiveCell(activeCell.row, activeCell.cell); + } +} + +function getTableHeight(rows: number): number { + return rows * TableRowHeight + TableHeaderRowHeight; +} diff --git a/src/sql/workbench/browser/designer/interfaces.ts b/src/sql/workbench/browser/designer/interfaces.ts index 9744e5e3b1..cffe47181a 100644 --- a/src/sql/workbench/browser/designer/interfaces.ts +++ b/src/sql/workbench/browser/designer/interfaces.ts @@ -104,6 +104,7 @@ export interface DesignerState { export const NameProperty = 'name'; export const ScriptProperty = 'script'; +export const CanBeDeletedProperty = 'canBeDeleted'; export interface DesignerView { components?: DesignerDataPropertyInfo[] @@ -194,10 +195,19 @@ export interface DesignerTableProperties extends ComponentProperties { * The confirmation message to be displayed when user removes a row. */ removeRowConfirmationMessage?: string; + /** + * Whether to show the item detail in properties view. The default value is true. + */ + showItemDetailInPropertiesView?: boolean; + /** + * The label of the add new button. The default value is 'Add New'. + */ + labelForAddNewButton?: string; } export interface DesignerTableComponentRowData { - [key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties; + [key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties | boolean; + canBeDeleted?: boolean; } @@ -211,8 +221,11 @@ export interface DesignerEdit { type: DesignerEditType; path: DesignerPropertyPath; value?: any; + source: DesignerUIArea; } +export type DesignerUIArea = 'PropertiesView' | 'ScriptView' | 'TopContentView' | 'TabsView'; + export type DesignerPropertyPath = (string | number)[]; export const DesignerRootObjectPath: DesignerPropertyPath = []; @@ -220,6 +233,7 @@ export type DesignerValidationError = { message: string, propertyPath?: Designer export interface DesignerEditResult { isValid: boolean; + refreshView?: boolean; errors?: DesignerValidationError[]; } diff --git a/src/sql/workbench/browser/designer/media/designer.css b/src/sql/workbench/browser/designer/media/designer.css index 8c32ddc58b..3cc203ddbb 100644 --- a/src/sql/workbench/browser/designer/media/designer.css +++ b/src/sql/workbench/browser/designer/media/designer.css @@ -73,6 +73,12 @@ overflow-y: auto; } +.designer-component .designer-tab-view { + overflow: scroll; + width: 100%; + height: 100%; +} + .designer-component .component-label { vertical-align: middle; } diff --git a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts index f7040fa8e6..ace7842b05 100644 --- a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts +++ b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerValidationError } from 'sql/workbench/browser/designer/interfaces'; +import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerValidationError, ScriptProperty } from 'sql/workbench/browser/designer/interfaces'; import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface'; import { localize } from 'vs/nls'; import { designers } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -90,14 +90,18 @@ export class TableDesignerComponentInput implements DesignerComponentInput { this._provider.processTableEdit(this.tableInfo, edit).then( result => { this._viewModel = result.viewModel; + if (result.view) { + this.setDesignerView(result.view); + } this._validationErrors = result.errors; - this.updateState(result.isValid, !equals(this._viewModel, this._originalViewModel), undefined); + this.updateState(result.isValid, this.isDirty(), undefined); this._onEditProcessed.fire({ edit: edit, result: { isValid: result.isValid, - errors: result.errors + errors: result.errors, + refreshView: !!result.view } }); editAction.withAdditionalMeasurements({ @@ -152,6 +156,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput { const result = await this._provider.publishChanges(this.tableInfo); this._viewModel = result.viewModel; this._originalViewModel = result.viewModel; + this.setDesignerView(result.view); saveNotificationHandle.updateMessage(localize('tableDesigner.publishChangeSuccess', "The changes have been successfully published.")); this.tableInfo = result.newTableInfo; this.updateState(true, false); @@ -185,7 +190,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput { return; } const dialog = this._instantiationService.createInstance(TableDesignerPublishDialog); - const result = await dialog.open(report); + const result = await dialog.open(report.report); if (result === TableDesignerPublishDialogResult.GenerateScript) { await this.generateScript(); } else if (result === TableDesignerPublishDialogResult.UpdateDatabase) { @@ -239,31 +244,35 @@ export class TableDesignerComponentInput implements DesignerComponentInput { this.updateState(true, this.tableInfo.isNewTable); this._viewModel = designerInfo.viewModel; this._originalViewModel = this.tableInfo.isNewTable ? undefined : deepClone(this._viewModel); - this.setDefaultData(); + this.setDesignerView(designerInfo.view); + } + private setDesignerView(tableDesignerView: azdata.designers.TableDesignerView) { const tabs = []; - if (designerInfo.view.columnTableOptions?.showTable) { - tabs.push(this.getColumnsTab(designerInfo.view.columnTableOptions, designerInfo.columnTypes)); + if (tableDesignerView.columnTableOptions?.showTable) { + tabs.push(this.getColumnsTab(tableDesignerView.columnTableOptions)); } - if (designerInfo.view.foreignKeyTableOptions?.showTable) { - tabs.push(this.getForeignKeysTab(designerInfo.view.foreignKeyTableOptions)); + tabs.push(this.getPrimaryKeyTab(tableDesignerView)); + + if (tableDesignerView.foreignKeyTableOptions?.showTable) { + tabs.push(this.getForeignKeysTab(tableDesignerView.foreignKeyTableOptions, tableDesignerView.foreignKeyColumnMappingTableOptions)); } - if (designerInfo.view.checkConstraintTableOptions?.showTable) { - tabs.push(this.getCheckConstraintsTab(designerInfo.view.checkConstraintTableOptions)); + if (tableDesignerView.checkConstraintTableOptions?.showTable) { + tabs.push(this.getCheckConstraintsTab(tableDesignerView.checkConstraintTableOptions)); } - if (designerInfo.view.indexTableOptions?.showTable) { - tabs.push(this.getIndexesTab(designerInfo.view.indexTableOptions, designerInfo.view.indexColumnSpecificationTableOptions)); + if (tableDesignerView.indexTableOptions?.showTable) { + tabs.push(this.getIndexesTab(tableDesignerView.indexTableOptions, tableDesignerView.indexColumnSpecificationTableOptions)); } - if (designerInfo.view.additionalTabs) { - tabs.push(...designerInfo.view.additionalTabs); + if (tableDesignerView.additionalTabs) { + tabs.push(...tableDesignerView.additionalTabs); } - tabs.push(this.getGeneralTab(designerInfo)); + tabs.push(this.getGeneralTab(tableDesignerView)); this._view = { components: [{ @@ -279,7 +288,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput { }; } - private getGeneralTab(designerInfo: azdata.designers.TableDesignerInfo): DesignerTab { + private getGeneralTab(tableDesignerView: azdata.designers.TableDesignerView): DesignerTab { const generalTabComponents: DesignerDataPropertyInfo[] = [ { componentType: 'dropdown', @@ -287,7 +296,6 @@ export class TableDesignerComponentInput implements DesignerComponentInput { description: localize('designer.table.description.schema', "The schema that contains the table."), componentProperties: { title: localize('tableDesigner.schemaTitle', "Schema"), - values: designerInfo.schemas } }, { componentType: 'input', @@ -299,8 +307,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput { } ]; - if (designerInfo.view.additionalTableProperties) { - generalTabComponents.push(...designerInfo.view.additionalTableProperties); + if (tableDesignerView.additionalTableProperties) { + generalTabComponents.push(...tableDesignerView.additionalTableProperties); } return { @@ -309,7 +317,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput { }; } - private getColumnsTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions, columnTypes: string[]): DesignerTab { + private getColumnsTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { const columnProperties: DesignerDataPropertyInfo[] = [ { @@ -326,8 +334,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput { description: localize('designer.column.description.dataType', "Displays the data type name for the column"), componentProperties: { title: localize('tableDesigner.columnTypeTitle', "Type"), - width: 100, - values: columnTypes + width: 100 } }, { componentType: 'input', @@ -404,14 +411,15 @@ export class TableDesignerComponentInput implements DesignerComponentInput { canAddRows: options.canAddRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, - showRemoveRowConfirmation: options.showRemoveRowConfirmation + showRemoveRowConfirmation: options.showRemoveRowConfirmation, + labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addNewColumn', "New Column") } } ] }; } - private getForeignKeysTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { + private getForeignKeysTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions, columnMappingTableOptions: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { const foreignKeyColumnMappingProperties: DesignerDataPropertyInfo[] = [ { @@ -476,13 +484,12 @@ export class TableDesignerComponentInput implements DesignerComponentInput { group: localize('tableDesigner.foreignKeyColumns', "Columns"), componentProperties: { ariaLabel: localize('tableDesigner.foreignKeyColumns', "Columns"), - columns: [designers.ForeignKeyColumnMappingProperty.Column, designers.ForeignKeyColumnMappingProperty.ForeignColumn], - itemProperties: foreignKeyColumnMappingProperties, - objectTypeDisplayName: '', - canAddRows: options.canAddRows, - canRemoveRows: options.canRemoveRows, - removeRowConfirmationMessage: options.removeRowConfirmationMessage, - showRemoveRowConfirmation: options.showRemoveRowConfirmation + columns: this.getTableDisplayProperties(columnMappingTableOptions, [designers.ForeignKeyColumnMappingProperty.Column, designers.ForeignKeyColumnMappingProperty.ForeignColumn]), + itemProperties: this.addAdditionalTableProperties(columnMappingTableOptions, foreignKeyColumnMappingProperties), + canAddRows: columnMappingTableOptions.canAddRows, + canRemoveRows: columnMappingTableOptions.canRemoveRows, + removeRowConfirmationMessage: columnMappingTableOptions.removeRowConfirmationMessage, + labelForAddNewButton: columnMappingTableOptions.labelForAddNewButton ?? localize('tableDesigner.addNewColumnMapping', "New Column Mapping") } } ]; @@ -502,13 +509,70 @@ export class TableDesignerComponentInput implements DesignerComponentInput { canAddRows: options.canAddRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, - showRemoveRowConfirmation: options.showRemoveRowConfirmation + showRemoveRowConfirmation: options.showRemoveRowConfirmation, + labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addForeignKey', "New Foreign Key") } } ] }; } + private getPrimaryKeyTab(view: azdata.designers.TableDesignerView): DesignerTab { + const options = view.primaryKeyColumnSpecificationTableOptions; + const columnSpecProperties: DesignerDataPropertyInfo[] = [ + { + componentType: 'dropdown', + propertyName: designers.TableIndexColumnSpecificationProperty.Column, + description: localize('designer.index.column.description.name', "The name of the column."), + componentProperties: { + title: localize('tableDesigner.index.column.name', "Column"), + width: 100 + } + }]; + + const tabComponents = []; + tabComponents.push( + { + componentType: 'input', + propertyName: designers.TableProperty.PrimaryKeyName, + showInPropertiesView: false, + description: localize('designer.table.primaryKeyName.description', "Name of the primary key."), + componentProperties: { + title: localize('tableDesigner.primaryKeyNameTitle', "Name") + } + }); + if (view.additionalPrimaryKeyProperties) { + view.additionalPrimaryKeyProperties.forEach(component => { + component.showInPropertiesView = false; + tabComponents.push(component); + }); + } + tabComponents.push({ + componentType: 'table', + propertyName: designers.TableProperty.PrimaryKeyColumns, + showInPropertiesView: false, + description: localize('designer.table.primaryKeyColumns.description', "Columns in the primary key."), + componentProperties: { + title: localize('tableDesigner.primaryKeyColumnsTitle', "Primary Key Columns"), + ariaLabel: localize('tableDesigner.primaryKeyColumnsTitle', "Primary Key Columns"), + columns: this.getTableDisplayProperties(options, [designers.TableIndexColumnSpecificationProperty.Column]), + itemProperties: this.addAdditionalTableProperties(options, columnSpecProperties), + objectTypeDisplayName: '', + canAddRows: options.canAddRows, + canRemoveRows: options.canRemoveRows, + removeRowConfirmationMessage: options.removeRowConfirmationMessage, + showRemoveRowConfirmation: options.showRemoveRowConfirmation, + showItemDetailInPropertiesView: false, + labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addNewColumnToPrimaryKey', "Add Column") + } + }); + + return { + title: localize('tableDesigner.PrimaryKeyTabTitle', "Primary Key"), + components: tabComponents + }; + } + private getCheckConstraintsTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { const checkConstraintProperties: DesignerDataPropertyInfo[] = [ { @@ -545,7 +609,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput { canAddRows: options.canAddRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, - showRemoveRowConfirmation: options.showRemoveRowConfirmation + showRemoveRowConfirmation: options.showRemoveRowConfirmation, + labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addNewCheckConstraint', "New Check Constraint") } } ] @@ -584,8 +649,9 @@ export class TableDesignerComponentInput implements DesignerComponentInput { objectTypeDisplayName: '', canAddRows: columnSpecTableOptions.canAddRows, canRemoveRows: columnSpecTableOptions.canRemoveRows, - removeRowConfirmationMessage: options.removeRowConfirmationMessage, - showRemoveRowConfirmation: options.showRemoveRowConfirmation + removeRowConfirmationMessage: columnSpecTableOptions.removeRowConfirmationMessage, + showRemoveRowConfirmation: columnSpecTableOptions.showRemoveRowConfirmation, + labelForAddNewButton: columnSpecTableOptions.labelForAddNewButton ?? localize('tableDesigner.addNewColumnToIndex', "Add Column") } } ]; @@ -605,7 +671,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput { canAddRows: options.canAddRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, - showRemoveRowConfirmation: options.showRemoveRowConfirmation + showRemoveRowConfirmation: options.showRemoveRowConfirmation, + labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addNewIndex', "New Index") } } ] @@ -623,19 +690,6 @@ export class TableDesignerComponentInput implements DesignerComponentInput { return properties; } - private setDefaultData(): void { - const properties = Object.keys(this._viewModel); - 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._viewModel[property] = {}; - } - } - private createTelemetryInfo(): ITelemetryEventProperties { let telemetryInfo = { provider: this._provider.providerId, @@ -645,6 +699,21 @@ export class TableDesignerComponentInput implements DesignerComponentInput { return telemetryInfo; } + private isDirty(): boolean { + const copyOfViewModel = deepClone(this._viewModel); + const copyOfOriginalViewModel = deepClone(this._originalViewModel); + // The generated script might be slightly different even though the models are the same + // espeically the order of the description property statements. + // we should take the script out for comparison. + if (copyOfViewModel) { + delete copyOfViewModel[ScriptProperty]; + } + if (copyOfOriginalViewModel) { + delete copyOfOriginalViewModel[ScriptProperty]; + } + return !equals(copyOfViewModel, copyOfOriginalViewModel); + } + /** * 1. 'Add' scenario a. ['propertyName1']. Example: add a column to the columns property: ['columns'].