table designer new features (#18682)

* support graph tables

* ignore script compare

* ability to refresh view after edit

* reserve focus after refresh view

* primary key and default constraint

* bug fixes

* vbump sts

* comments

* update type

* fix issue
This commit is contained in:
Alan Ren
2022-03-09 14:17:01 -08:00
committed by GitHub
parent 27763c860c
commit e50bded5d1
19 changed files with 352 additions and 153 deletions

View File

@@ -1,6 +1,6 @@
{ {
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", "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": { "downloadFileNames": {
"Windows_86": "win-x86-net6.0.zip", "Windows_86": "win-x86-net6.0.zip",
"Windows_64": "win-x64-net6.0.zip", "Windows_64": "win-x64-net6.0.zip",

View File

@@ -1096,7 +1096,7 @@ export namespace InitializeTableDesignerRequest {
} }
export namespace ProcessTableDesignerEditRequest { export namespace ProcessTableDesignerEditRequest {
export const type = new RequestType<TableDesignerEditRequestParams, azdata.designers.DesignerEditResult, void, void>('tabledesigner/processedit'); export const type = new RequestType<TableDesignerEditRequestParams, azdata.designers.DesignerEditResult<azdata.designers.TableDesignerView>, void, void>('tabledesigner/processedit');
} }
export namespace PublishTableDesignerChangesRequest { export namespace PublishTableDesignerChangesRequest {

View File

@@ -1117,7 +1117,7 @@ export class TableDesignerFeature extends SqlOpsFeature<undefined> {
return Promise.reject(e); return Promise.reject(e);
} }
}; };
const processTableEdit = (tableInfo: azdata.designers.TableInfo, tableChangeInfo: azdata.designers.DesignerEdit): Thenable<azdata.designers.DesignerEditResult> => { const processTableEdit = (tableInfo: azdata.designers.TableInfo, tableChangeInfo: azdata.designers.DesignerEdit): Thenable<azdata.designers.DesignerEditResult<azdata.designers.TableDesignerView>> => {
let params: contracts.TableDesignerEditRequestParams = { let params: contracts.TableDesignerEditRequestParams = {
tableInfo: tableInfo, tableInfo: tableInfo,
tableChangeInfo: tableChangeInfo tableChangeInfo: tableChangeInfo

View File

@@ -1081,7 +1081,7 @@ declare module 'azdata' {
* @param table the table information * @param table the table information
* @param tableChangeInfo the information about the change user made through the UI. * @param tableChangeInfo the information about the change user made through the UI.
*/ */
processTableEdit(table: TableInfo, tableChangeInfo: DesignerEdit): Thenable<DesignerEditResult>; processTableEdit(table: TableInfo, tableChangeInfo: DesignerEdit): Thenable<DesignerEditResult<TableDesignerView>>;
/** /**
* Publish the changes. * Publish the changes.
@@ -1154,14 +1154,6 @@ declare module 'azdata' {
* The initial state of the designer. * The initial state of the designer.
*/ */
viewModel: DesignerViewModel; 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', Script = 'script',
ForeignKeys = 'foreignKeys', ForeignKeys = 'foreignKeys',
CheckConstraints = 'checkConstraints', CheckConstraints = 'checkConstraints',
Indexes = 'indexes' Indexes = 'indexes',
PrimaryKeyName = 'primaryKeyName',
PrimaryKeyColumns = 'primaryKeyColumns'
} }
/** /**
* Name of the common table column properties. * Name of the common table column properties.
@@ -1262,6 +1256,12 @@ declare module 'azdata' {
* Default columns to display values are: Name, PrimaryKeyTable. * Default columns to display values are: Name, PrimaryKeyTable.
*/ */
foreignKeyTableOptions?: TableDesignerBuiltInTableViewOptions; 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. * Check constraints table options.
* Common check constraint properties are handled by Azure Data Studio. see {@link TableCheckConstraintProperty} * 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. * Default columns to display values are: Name.
*/ */
indexTableOptions?: TableDesignerBuiltInTableViewOptions; indexTableOptions?: TableDesignerBuiltInTableViewOptions;
/** /**
* Index column specification table options. * Index column specification table options.
* Common index properties are handled by Azure Data Studio. see {@link TableIndexColumnSpecificationProperty} * Common index properties are handled by Azure Data Studio. see {@link TableIndexColumnSpecificationProperty}
* Default columns to display values are: Column. * Default columns to display values are: Column.
*/ */
indexColumnSpecificationTableOptions?: TableDesignerBuiltInTableViewOptions; 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 { export interface TableDesignerBuiltInTableViewOptions extends DesignerTablePropertiesBase {
@@ -1371,6 +1380,14 @@ declare module 'azdata' {
* The confirmation message to be displayed when user removes a row. * The confirmation message to be displayed when user removes a row.
*/ */
removeRowConfirmationMessage?: string; 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. * The data item of the designer's table component.
*/ */
export interface DesignerTableComponentDataItem { 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. * The result returned by the table designer provider after handling an edit request.
*/ */
export interface DesignerEditResult { export interface DesignerEditResult<T> {
/**
* The new view information if the view needs to be refreshed.
*/
view?: T;
/** /**
* The view model object. * The view model object.
*/ */
@@ -1485,6 +1510,10 @@ declare module 'azdata' {
* The new view model. * The new view model.
*/ */
viewModel: DesignerViewModel; viewModel: DesignerViewModel;
/**
* The new view.
*/
view: TableDesignerView;
} }
} }

View File

@@ -5,7 +5,7 @@
import 'vs/css!./media/buttonColumn.plugin'; import 'vs/css!./media/buttonColumn.plugin';
import 'vs/css!./media/iconColumn'; 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'; import { escape } from 'sql/base/common/strings';
export interface ButtonCellValue { export interface ButtonCellValue {
@@ -13,7 +13,7 @@ export interface ButtonCellValue {
title: string; title: string;
} }
export interface ButtonColumnOptions extends IconColumnOptions { export interface ButtonColumnOptions extends IconColumnOptions, ClickableColumnOptions {
/** /**
* Whether to show the text. * Whether to show the text.
*/ */
@@ -23,7 +23,7 @@ export interface ButtonColumnOptions extends IconColumnOptions {
export class ButtonColumn<T extends Slick.SlickData> extends BaseClickableColumn<T> { export class ButtonColumn<T extends Slick.SlickData> extends BaseClickableColumn<T> {
constructor(private options: ButtonColumnOptions) { constructor(private options: ButtonColumnOptions) {
super(); super(options);
} }
public get definition(): Slick.Column<T> { public get definition(): Slick.Column<T> {
@@ -39,7 +39,8 @@ export class ButtonColumn<T extends Slick.SlickData> extends BaseClickableColumn
} }
const buttonTypeCssClass = this.options.showText ? 'slick-plugin-button slick-plugin-text-button' : 'slick-plugin-button slick-plugin-image-only-button'; 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 : ''; const buttonText = this.options.showText ? escapedTitle : '';
return `<button tabindex=-1 class="${iconCssClasses} ${buttonTypeCssClass}" title="${escapedTitle}" aria-label="${escapedTitle}">${buttonText}</button>`; const disabledAttribute = this.isCellEnabled(row, cell) ? '' : 'disabled';
return `<button tabindex=-1 class="${iconCssClasses} ${buttonTypeCssClass}" title="${escapedTitle}" aria-label="${escapedTitle}" ${disabledAttribute}>${buttonText}</button>`;
}, },
name: this.options.name, name: this.options.name,
resizable: this.options.resizable, resizable: this.options.resizable,

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/hyperlinkColumn.plugin'; import 'vs/css!./media/hyperlinkColumn.plugin';
import 'vs/css!./media/iconColumn'; 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'; import { escape } from 'sql/base/common/strings';
export interface HyperlinkCellValue { export interface HyperlinkCellValue {
@@ -13,12 +13,12 @@ export interface HyperlinkCellValue {
url?: string; url?: string;
} }
export interface HyperlinkColumnOptions extends IconColumnOptions { export interface HyperlinkColumnOptions extends IconColumnOptions, ClickableColumnOptions {
} }
export class HyperlinkColumn<T extends Slick.SlickData> extends BaseClickableColumn<T> { export class HyperlinkColumn<T extends Slick.SlickData> extends BaseClickableColumn<T> {
constructor(private options: HyperlinkColumnOptions) { constructor(private options: HyperlinkColumnOptions) {
super(); super(options);
} }
public get definition(): Slick.Column<T> { public get definition(): Slick.Column<T> {
@@ -31,7 +31,8 @@ export class HyperlinkColumn<T extends Slick.SlickData> extends BaseClickableCol
const cellValue = dataContext[this.options.field] as HyperlinkCellValue; const cellValue = dataContext[this.options.field] as HyperlinkCellValue;
const cssClasses = iconValue.iconCssClass ? `codicon icon slick-plugin-icon ${iconValue.iconCssClass}` : ''; const cssClasses = iconValue.iconCssClass ? `codicon icon slick-plugin-icon ${iconValue.iconCssClass}` : '';
const urlPart = cellValue?.url ? `href="${encodeURI(cellValue.url)}" target="blank"` : ''; const urlPart = cellValue?.url ? `href="${encodeURI(cellValue.url)}" target="blank"` : '';
return `<a ${urlPart} class="slick-hyperlink-cell ${cssClasses}" tabindex=-1 title="${escapedTitle}" aria-label="${escapedTitle}">${escapedTitle}</a>`; const disabledAttribute = this.isCellEnabled(row, cell) ? '' : 'disabled';
return `<a ${urlPart} class="slick-hyperlink-cell ${cssClasses}" tabindex=-1 title="${escapedTitle}" aria-label="${escapedTitle}" ${disabledAttribute}>${escapedTitle}</a>`;
}, },
name: this.options.name, name: this.options.name,
resizable: true, resizable: true,

View File

@@ -12,6 +12,11 @@
padding-bottom: 0px; padding-bottom: 0px;
} }
.slick-plugin-button:disabled {
cursor: not-allowed;
opacity: 0.4;
}
.slick-plugin-button.slick-plugin-text-button { .slick-plugin-button.slick-plugin-text-button {
border-width: 1px; border-width: 1px;
width: 100%; width: 100%;
@@ -27,4 +32,3 @@
border-width: 0px; border-width: 0px;
background-color: transparent; background-color: transparent;
} }

View File

@@ -17,13 +17,20 @@ export interface TableCellClickEventArgs<T extends Slick.SlickData> {
column: number; 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<T extends Slick.SlickData> implements Slick.Plugin<T>, TableColumn<T> { export abstract class BaseClickableColumn<T extends Slick.SlickData> implements Slick.Plugin<T>, TableColumn<T> {
private _handler = new Slick.EventHandler(); private _handler = new Slick.EventHandler();
private _grid!: Slick.Grid<T>; private _grid!: Slick.Grid<T>;
private _onClick = new Emitter<TableCellClickEventArgs<T>>(); private _onClick = new Emitter<TableCellClickEventArgs<T>>();
public onClick = this._onClick.event; public onClick = this._onClick.event;
constructor() { constructor(private readonly _options: ClickableColumnOptions) {
} }
public init(grid: Slick.Grid<T>): void { public init(grid: Slick.Grid<T>): void {
@@ -46,7 +53,7 @@ export abstract class BaseClickableColumn<T extends Slick.SlickData> implements
public abstract get definition(): Slick.Column<T>; public abstract get definition(): Slick.Column<T>;
private handleActiveCellChanged(args: Slick.OnActiveCellChangedEventArgs<T>): void { private handleActiveCellChanged(args: Slick.OnActiveCellChangedEventArgs<T>): void {
if (this.isCurrentColumn(args.cell)) { if (this.isCellEnabled(args.row, args.cell)) {
const cellElement = this._grid.getActiveCellNode(); const cellElement = this._grid.getActiveCellNode();
if (cellElement && cellElement.children) { if (cellElement && cellElement.children) {
const element = cellElement.children[0] as HTMLElement; const element = cellElement.children[0] as HTMLElement;
@@ -56,7 +63,7 @@ export abstract class BaseClickableColumn<T extends Slick.SlickData> implements
} }
private handleClick(args: Slick.OnClickEventArgs<T>): void { private handleClick(args: Slick.OnClickEventArgs<T>): void {
if (this.isCurrentColumn(args.cell)) { if (this.isCellEnabled(args.row, args.cell)) {
// SlickGrid will automatically set active cell on mouse click event, // 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, // 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. // 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<T extends Slick.SlickData> implements
private handleKeyboardEvent(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs<T>): void { private handleKeyboardEvent(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs<T>): void {
let event = new StandardKeyboardEvent(e); 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.stopPropagation();
event.preventDefault(); event.preventDefault();
this.fireClickEvent(); this.fireClickEvent();
@@ -92,8 +99,11 @@ export abstract class BaseClickableColumn<T extends Slick.SlickData> implements
} }
} }
private isCurrentColumn(columnIndex: number): boolean { protected isCellEnabled(row: number, cell: number): boolean {
return this._grid.getColumns()[columnIndex]?.id === this.definition.id; 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;
} }
} }

View File

@@ -516,7 +516,7 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData
initializeTableDesigner(tableInfo: azdata.designers.TableInfo): Thenable<azdata.designers.TableDesignerInfo> { initializeTableDesigner(tableInfo: azdata.designers.TableInfo): Thenable<azdata.designers.TableDesignerInfo> {
return self._proxy.$initializeTableDesigner(handle, tableInfo); return self._proxy.$initializeTableDesigner(handle, tableInfo);
}, },
processTableEdit(table, edit): Thenable<azdata.designers.DesignerEditResult> { processTableEdit(table, edit): Thenable<azdata.designers.DesignerEditResult<azdata.designers.TableDesignerView>> {
return self._proxy.$processTableDesignerEdit(handle, table, edit); return self._proxy.$processTableDesignerEdit(handle, table, edit);
}, },
publishChanges(tableInfo: azdata.designers.TableInfo): Thenable<azdata.designers.PublishChangesResult> { publishChanges(tableInfo: azdata.designers.TableInfo): Thenable<azdata.designers.PublishChangesResult> {

View File

@@ -897,7 +897,7 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
return this._resolveProvider<azdata.designers.TableDesignerProvider>(handle).initializeTableDesigner(table); return this._resolveProvider<azdata.designers.TableDesignerProvider>(handle).initializeTableDesigner(table);
} }
public override $processTableDesignerEdit(handle: number, table: azdata.designers.TableInfo, edit: azdata.designers.DesignerEdit): Thenable<azdata.designers.DesignerEditResult> { public override $processTableDesignerEdit(handle: number, table: azdata.designers.TableInfo, edit: azdata.designers.DesignerEdit): Thenable<azdata.designers.DesignerEditResult<azdata.designers.TableDesignerView>> {
return this._resolveProvider<azdata.designers.TableDesignerProvider>(handle).processTableEdit(table, edit); return this._resolveProvider<azdata.designers.TableDesignerProvider>(handle).processTableEdit(table, edit);
} }

View File

@@ -541,7 +541,7 @@ export abstract class ExtHostDataProtocolShape {
/** /**
* Process the table edit. * Process the table edit.
*/ */
$processTableDesignerEdit(handle: number, table: azdata.designers.TableInfo, edit: azdata.designers.DesignerEdit): Thenable<azdata.designers.DesignerEditResult> { throw ni(); } $processTableDesignerEdit(handle: number, table: azdata.designers.TableInfo, edit: azdata.designers.DesignerEdit): Thenable<azdata.designers.DesignerEditResult<azdata.designers.TableDesignerView>> { throw ni(); }
/** /**
* Publish the table designer changes. * Publish the table designer changes.

View File

@@ -965,7 +965,9 @@ export namespace designers {
Script = 'script', Script = 'script',
ForeignKeys = 'foreignKeys', ForeignKeys = 'foreignKeys',
CheckConstraints = 'checkConstraints', CheckConstraints = 'checkConstraints',
Indexes = 'indexes' Indexes = 'indexes',
PrimaryKeyName = 'primaryKeyName',
PrimaryKeyColumns = 'primaryKeyColumns'
} }
export enum TableColumnProperty { export enum TableColumnProperty {

View File

@@ -6,7 +6,7 @@
import { import {
DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerPropertyPath, DesignerViewModel, DesignerDataPropertyInfo, DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerPropertyPath, DesignerViewModel, DesignerDataPropertyInfo,
DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties, DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties,
DesignerEditProcessedEventArgs, DesignerStateChangedEventArgs, DesignerAction, DesignerUIState, ScriptProperty, DesignerRootObjectPath DesignerEditProcessedEventArgs, DesignerStateChangedEventArgs, DesignerAction, ScriptProperty, DesignerRootObjectPath, CanBeDeletedProperty, DesignerUIArea
} }
from 'sql/workbench/browser/designer/interfaces'; from 'sql/workbench/browser/designer/interfaces';
import { IPanelTab, ITabbedPanelStyles, TabbedPanel } from 'sql/base/browser/ui/panel/panel'; 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 { IThemeService } from 'vs/platform/theme/common/themeService';
import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry';
import { alert } from 'vs/base/browser/ui/aria/aria'; 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 { Dropdown, IDropdownStyles } from 'sql/base/browser/ui/editableDropdown/browser/dropdown';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
@@ -62,13 +63,14 @@ export type DesignerUIComponent = InputBox | Checkbox | Table<Slick.SlickData> |
export type CreateComponentsFunc = (container: HTMLElement, components: DesignerDataPropertyInfo[], parentPath: DesignerPropertyPath) => DesignerUIComponent[]; export type CreateComponentsFunc = (container: HTMLElement, components: DesignerDataPropertyInfo[], parentPath: DesignerPropertyPath) => DesignerUIComponent[];
export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void; export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void;
const TableRowHeight = 25; interface DesignerTableCellContext {
const TableHeaderRowHeight = 28; view: DesignerUIArea;
path: DesignerPropertyPath;
}
const ScriptTabId = 'scripts'; const ScriptTabId = 'scripts';
const MessagesTabId = 'messages'; const MessagesTabId = 'messages';
type DesignerUIArea = 'PropertiesView' | 'ScriptView' | 'TopContentView' | 'TabsView';
export class Designer extends Disposable implements IThemable { export class Designer extends Disposable implements IThemable {
private _loadingSpinner: LoadingSpinner; private _loadingSpinner: LoadingSpinner;
private _horizontalSplitViewContainer: HTMLElement; private _horizontalSplitViewContainer: HTMLElement;
@@ -107,11 +109,12 @@ export class Designer extends Disposable implements IThemable {
valueGetter: (item, column): string => { valueGetter: (item, column): string => {
return item[column.field].value; return item[column.field].value;
}, },
valueSetter: (parentPath: DesignerPropertyPath, row: number, item: DesignerTableComponentRowData, column: Slick.Column<Slick.SlickData>, value: string): void => { valueSetter: (context: DesignerTableCellContext, row: number, item: DesignerTableComponentRowData, column: Slick.Column<Slick.SlickData>, value: string): void => {
this.handleEdit({ this.handleEdit({
type: DesignerEditType.Update, type: DesignerEditType.Update,
path: [...parentPath, row, column.field], path: [...context.path, row, column.field],
value: value value: value,
source: context.view
}); });
}, },
optionsGetter: (item, column): string[] => { optionsGetter: (item, column): string[] => {
@@ -148,7 +151,9 @@ export class Designer extends Disposable implements IThemable {
this._scriptTabbedPannel = new TabbedPanel(this._editorContainer); this._scriptTabbedPannel = new TabbedPanel(this._editorContainer);
this._messagesView = this._instantiationService.createInstance(DesignerMessagesTabPanelView); this._messagesView = this._instantiationService.createInstance(DesignerMessagesTabPanelView);
this._register(this._messagesView.onMessageSelected((path) => { this._register(this._messagesView.onMessageSelected((path) => {
this.selectProperty(path); if (path && path.length > 0) {
this.selectProperty(path);
}
})); }));
this._scriptEditorView = new DesignerScriptEditorTabPanelView(this._instantiationService); this._scriptEditorView = new DesignerScriptEditorTabPanelView(this._instantiationService);
this._scriptTabbedPannel.pushTab({ this._scriptTabbedPannel.pushTab({
@@ -258,24 +263,12 @@ export class Designer extends Disposable implements IThemable {
public setInput(input: DesignerComponentInput): void { public setInput(input: DesignerComponentInput): void {
// Save state this.saveUIState();
if (this._input) {
this._input.designerUIState = this.getUIState();
}
// Clean up
if (this._loadingTimeoutHandle) { if (this._loadingTimeoutHandle) {
this.stopLoading(); this.stopLoading();
} }
this._buttons = []; this.clearUI();
this._componentMap.clear();
DOM.clearNode(this._topContentContainer);
this._contentTabbedPanel.clearTabs();
this._propertiesPane.clear();
this._inputDisposable?.dispose(); this._inputDisposable?.dispose();
this._groupHeaders = [];
// Initialize with new input // Initialize with new input
this._input = input; this._input = input;
this._inputDisposable = new DisposableStore(); this._inputDisposable = new DisposableStore();
@@ -307,6 +300,17 @@ export class Designer extends Disposable implements IThemable {
this._inputDisposable?.dispose(); 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 { private initializeDesigner(): void {
const view = this._input.view; const view = this._input.view;
if (view.components) { if (view.components) {
@@ -315,8 +319,8 @@ export class Designer extends Disposable implements IThemable {
view.tabs.forEach(tab => { view.tabs.forEach(tab => {
this._contentTabbedPanel.pushTab(this.createTabView(tab)); this._contentTabbedPanel.pushTab(this.createTabView(tab));
}); });
this.layoutTabbedPanel();
this.updateComponentValues(); this.updateComponentValues();
this.layoutTabbedPanel();
this.updatePropertiesPane(DesignerRootObjectPath); this.updatePropertiesPane(DesignerRootObjectPath);
this.restoreUIState(); 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)); alert(localize('designer.errorCountAlert', "{0} validation errors found.", args.result.errors.length));
} }
try { 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) { 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. // 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) { if (edit.path.length === 1) {
@@ -353,6 +365,10 @@ export class Designer extends Disposable implements IThemable {
this.updatePropertiesPane(this._propertiesPane.objectPath); 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) { } catch (err) {
this._notificationService.error(err); this._notificationService.error(err);
} }
@@ -397,8 +413,9 @@ export class Designer extends Disposable implements IThemable {
} }
private refresh() { private refresh() {
this.updateComponentValues(); this.saveUIState();
this.updatePropertiesPane(this._propertiesPane.objectPath); this.clearUI();
this.initializeDesigner();
} }
private layoutTabbedPanel() { private layoutTabbedPanel() {
@@ -408,21 +425,11 @@ export class Designer extends Disposable implements IThemable {
private layoutPropertiesPane() { private layoutPropertiesPane() {
this._propertiesPane?.componentMap.forEach((v) => { this._propertiesPane?.componentMap.forEach((v) => {
if (v.component instanceof Table) { if (v.component instanceof Table) {
const rows = v.component.getData().getLength(); layoutDesignerTable(v.component, this._propertiesPaneContainer.clientWidth);
// 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));
} }
}); });
} }
private getTableHeight(rows: number): number {
return rows * TableRowHeight + TableHeaderRowHeight;
}
private updatePropertiesPane(objectPath: DesignerPropertyPath): void { private updatePropertiesPane(objectPath: DesignerPropertyPath): void {
let type: string; let type: string;
let components: DesignerDataPropertyInfo[]; let components: DesignerDataPropertyInfo[];
@@ -485,7 +492,7 @@ export class Designer extends Disposable implements IThemable {
this._messagesView.updateMessages(this._input.validationErrors); 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)) { if (!DesignerPropertyPathValidator.validate(path, this._input.viewModel)) {
return; return;
} }
@@ -505,7 +512,10 @@ export class Designer extends Disposable implements IThemable {
if (tab) { if (tab) {
for (const component of tab.components) { for (const component of tab.components) {
if (path[0] === component.propertyName) { 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; found = true;
break; break;
} }
@@ -520,7 +530,12 @@ export class Designer extends Disposable implements IThemable {
if (found) { if (found) {
const propertyInfo = this._componentMap.get(<string>path[0]); const propertyInfo = this._componentMap.get(<string>path[0]);
if (propertyInfo.defintion.componentType !== 'table') { if (propertyInfo.defintion.componentType !== 'table') {
propertyInfo.component.focus(); if (view === 'PropertiesView') {
this.updatePropertiesPane(DesignerRootObjectPath);
this._propertiesPane.selectProperty(path);
} else {
propertyInfo.component.focus();
}
return; return;
} else { } else {
const tableComponent = <Table<Slick.SlickData>>propertyInfo.component; const tableComponent = <Table<Slick.SlickData>>propertyInfo.component;
@@ -533,7 +548,9 @@ export class Designer extends Disposable implements IThemable {
this._propertiesPane.selectProperty(relativePath); this._propertiesPane.selectProperty(relativePath);
} }
} }
this.highlightActiveElement(); if (highlight) {
this.highlightActiveElement();
}
} }
} }
@@ -703,7 +720,7 @@ export class Designer extends Disposable implements IThemable {
}); });
input.onLoseFocus((args) => { input.onLoseFocus((args) => {
if (args.hasChanged) { 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(() => { 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 = new Dropdown(dropdownContainer, this._contextViewProvider, { values: dropdownProperties.values as string[] || [] });
dropdown.ariaLabel = componentDefinition.componentProperties?.title; dropdown.ariaLabel = componentDefinition.componentProperties?.title;
dropdown.onValueChange((value) => { 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(() => { dropdown.onFocus(() => {
if (view === 'PropertiesView') { if (view === 'PropertiesView') {
@@ -742,7 +759,7 @@ export class Designer extends Disposable implements IThemable {
dropdown.render(dropdownContainer); dropdown.render(dropdownContainer);
dropdown.selectElem.style.height = '25px'; dropdown.selectElem.style.height = '25px';
dropdown.onDidSelect((e) => { 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(() => { dropdown.onDidFocus(() => {
if (view === 'PropertiesView') { if (view === 'PropertiesView') {
@@ -760,7 +777,7 @@ export class Designer extends Disposable implements IThemable {
const checkboxProperties = componentDefinition.componentProperties as CheckBoxProperties; const checkboxProperties = componentDefinition.componentProperties as CheckBoxProperties;
const checkbox = new Checkbox(checkboxContainer, { label: '', ariaLabel: checkboxProperties.title }); const checkbox = new Checkbox(checkboxContainer, { label: '', ariaLabel: checkboxProperties.title });
checkbox.onChange((newValue) => { 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(() => { checkbox.onFocus(() => {
if (view === 'PropertiesView') { if (view === 'PropertiesView') {
@@ -778,7 +795,7 @@ export class Designer extends Disposable implements IThemable {
const tableProperties = componentDefinition.componentProperties as DesignerTableProperties; const tableProperties = componentDefinition.componentProperties as DesignerTableProperties;
if (tableProperties.canAddRows) { if (tableProperties.canAddRows) {
const buttonContainer = container.appendChild(DOM.$('.full-row')).appendChild(DOM.$('.add-row-button-container')); 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, { const addRowButton = new Button(buttonContainer, {
title: addNewText, title: addNewText,
secondary: true secondary: true
@@ -786,7 +803,8 @@ export class Designer extends Disposable implements IThemable {
addRowButton.onDidClick(() => { addRowButton.onDidClick(() => {
this.handleEdit({ this.handleEdit({
type: DesignerEditType.Add, type: DesignerEditType.Add,
path: propertyPath path: propertyPath,
source: view
}); });
}); });
this.styleComponent(addRowButton); this.styleComponent(addRowButton);
@@ -829,7 +847,8 @@ export class Designer extends Disposable implements IThemable {
this.handleEdit({ this.handleEdit({
type: DesignerEditType.Update, type: DesignerEditType.Update,
path: [...propertyPath, e.row, propertyDefinition.propertyName], path: [...propertyPath, e.row, propertyDefinition.propertyName],
value: e.value value: e.value,
source: view
}); });
}); });
return checkboxColumn.definition; return checkboxColumn.definition;
@@ -838,7 +857,7 @@ export class Designer extends Disposable implements IThemable {
return { return {
name: dropdownProperties.title, name: dropdownProperties.title,
field: propertyDefinition.propertyName, 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 width: dropdownProperties.width as number
}; };
default: default:
@@ -846,7 +865,7 @@ export class Designer extends Disposable implements IThemable {
return { return {
name: inputProperties.title, name: inputProperties.title,
field: propertyDefinition.propertyName, 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 width: inputProperties.width as number
}; };
} }
@@ -858,7 +877,8 @@ export class Designer extends Disposable implements IThemable {
title: localize('designer.removeRowText', "Remove"), title: localize('designer.removeRowText', "Remove"),
width: 20, width: 20,
resizable: false, resizable: false,
isFontIcon: true isFontIcon: true,
enabledField: CanBeDeletedProperty
}); });
deleteRowColumn.onClick(async (e) => { deleteRowColumn.onClick(async (e) => {
if (tableProperties.showRemoveRowConfirmation) { if (tableProperties.showRemoveRowConfirmation) {
@@ -873,7 +893,8 @@ export class Designer extends Disposable implements IThemable {
} }
this.handleEdit({ this.handleEdit({
type: DesignerEditType.Remove, type: DesignerEditType.Remove,
path: [...propertyPath, e.row] path: [...propertyPath, e.row],
source: view
}); });
}); });
table.registerPlugin(deleteRowColumn); table.registerPlugin(deleteRowColumn);
@@ -887,7 +908,11 @@ export class Designer extends Disposable implements IThemable {
table.grid.onActiveCellChanged.subscribe((e, data) => { table.grid.onActiveCellChanged.subscribe((e, data) => {
if (view === 'TabsView' || view === 'TopContentView') { if (view === 'TabsView' || view === 'TopContentView') {
if (data.row !== undefined) { if (data.row !== undefined) {
this.updatePropertiesPane([...propertyPath, data.row]); if (tableProperties.showItemDetailInPropertiesView === false) {
this.updatePropertiesPane(DesignerRootObjectPath);
} else {
this.updatePropertiesPane([...propertyPath, data.row]);
}
} else { } else {
this.updatePropertiesPane(DesignerRootObjectPath); this.updatePropertiesPane(DesignerRootObjectPath);
} }
@@ -934,11 +959,13 @@ export class Designer extends Disposable implements IThemable {
} }
} }
private getUIState(): DesignerUIState { private saveUIState(): void {
return { if (this._input) {
activeContentTabId: this._contentTabbedPanel.activeTabId, this._input.designerUIState = {
activeScriptTabId: this._scriptTabbedPannel.activeTabId activeContentTabId: this._contentTabbedPanel.activeTabId,
}; activeScriptTabId: this._scriptTabbedPannel.activeTabId
};
}
} }
private restoreUIState(): void { private restoreUIState(): void {

View File

@@ -56,7 +56,7 @@ export class DesignerPropertyPathValidator {
if (!tableData.data || tableData.data.length - 1 < objectIndex) { if (!tableData.data || tableData.data.length - 1 < objectIndex) {
return false; return false;
} }
currentObject = tableData.data[objectIndex]; currentObject = tableData.data[objectIndex] as DesignerViewModel;
index++; index++;
} }
return true; return true;

View File

@@ -9,17 +9,16 @@ import { Table } from 'sql/base/browser/ui/table/table';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom'; import * as DOM from 'vs/base/browser/dom';
import { CreateComponentsFunc } from 'sql/workbench/browser/designer/designer'; import { CreateComponentsFunc } from 'sql/workbench/browser/designer/designer';
import { layoutDesignerTable } from 'sql/workbench/browser/designer/designerTableUtil';
const ButtonHeight = 30;
const HorizontalPadding = 10;
const VerticalPadding = 20;
export class DesignerTabPanelView extends Disposable implements IPanelView { export class DesignerTabPanelView extends Disposable implements IPanelView {
private _viewContainer: HTMLElement;
private _componentsContainer: HTMLElement; private _componentsContainer: HTMLElement;
private _tables: Table<Slick.SlickData>[] = []; private _tables: Table<Slick.SlickData>[] = [];
constructor(private readonly _tab: DesignerTab, private _createComponents: CreateComponentsFunc) { constructor(private readonly _tab: DesignerTab, private _createComponents: CreateComponentsFunc) {
super(); 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); const uiComponents = this._createComponents(this._componentsContainer, this._tab.components, DesignerRootObjectPath);
uiComponents.forEach(component => { uiComponents.forEach(component => {
if (component instanceof Table) { if (component instanceof Table) {
@@ -29,12 +28,12 @@ export class DesignerTabPanelView extends Disposable implements IPanelView {
} }
render(container: HTMLElement): void { render(container: HTMLElement): void {
container.appendChild(this._componentsContainer); container.appendChild(this._viewContainer);
} }
layout(dimension: DOM.Dimension): void { layout(dimension: DOM.Dimension): void {
this._tables.forEach(table => { this._tables.forEach(table => {
table.layout(new DOM.Dimension(dimension.width - HorizontalPadding, dimension.height - VerticalPadding - ButtonHeight)); layoutDesignerTable(table, dimension.width);
}); });
} }
} }

View File

@@ -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<Slick.SlickData>, 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;
}

View File

@@ -104,6 +104,7 @@ export interface DesignerState {
export const NameProperty = 'name'; export const NameProperty = 'name';
export const ScriptProperty = 'script'; export const ScriptProperty = 'script';
export const CanBeDeletedProperty = 'canBeDeleted';
export interface DesignerView { export interface DesignerView {
components?: DesignerDataPropertyInfo[] components?: DesignerDataPropertyInfo[]
@@ -194,10 +195,19 @@ export interface DesignerTableProperties extends ComponentProperties {
* The confirmation message to be displayed when user removes a row. * The confirmation message to be displayed when user removes a row.
*/ */
removeRowConfirmationMessage?: string; 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 { 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; type: DesignerEditType;
path: DesignerPropertyPath; path: DesignerPropertyPath;
value?: any; value?: any;
source: DesignerUIArea;
} }
export type DesignerUIArea = 'PropertiesView' | 'ScriptView' | 'TopContentView' | 'TabsView';
export type DesignerPropertyPath = (string | number)[]; export type DesignerPropertyPath = (string | number)[];
export const DesignerRootObjectPath: DesignerPropertyPath = []; export const DesignerRootObjectPath: DesignerPropertyPath = [];
@@ -220,6 +233,7 @@ export type DesignerValidationError = { message: string, propertyPath?: Designer
export interface DesignerEditResult { export interface DesignerEditResult {
isValid: boolean; isValid: boolean;
refreshView?: boolean;
errors?: DesignerValidationError[]; errors?: DesignerValidationError[];
} }

View File

@@ -73,6 +73,12 @@
overflow-y: auto; overflow-y: auto;
} }
.designer-component .designer-tab-view {
overflow: scroll;
width: 100%;
height: 100%;
}
.designer-component .component-label { .designer-component .component-label {
vertical-align: middle; vertical-align: middle;
} }

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; 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 { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { designers } from 'sql/workbench/api/common/sqlExtHostTypes'; import { designers } from 'sql/workbench/api/common/sqlExtHostTypes';
@@ -90,14 +90,18 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
this._provider.processTableEdit(this.tableInfo, edit).then( this._provider.processTableEdit(this.tableInfo, edit).then(
result => { result => {
this._viewModel = result.viewModel; this._viewModel = result.viewModel;
if (result.view) {
this.setDesignerView(result.view);
}
this._validationErrors = result.errors; this._validationErrors = result.errors;
this.updateState(result.isValid, !equals(this._viewModel, this._originalViewModel), undefined); this.updateState(result.isValid, this.isDirty(), undefined);
this._onEditProcessed.fire({ this._onEditProcessed.fire({
edit: edit, edit: edit,
result: { result: {
isValid: result.isValid, isValid: result.isValid,
errors: result.errors errors: result.errors,
refreshView: !!result.view
} }
}); });
editAction.withAdditionalMeasurements({ editAction.withAdditionalMeasurements({
@@ -152,6 +156,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
const result = await this._provider.publishChanges(this.tableInfo); const result = await this._provider.publishChanges(this.tableInfo);
this._viewModel = result.viewModel; this._viewModel = result.viewModel;
this._originalViewModel = result.viewModel; this._originalViewModel = result.viewModel;
this.setDesignerView(result.view);
saveNotificationHandle.updateMessage(localize('tableDesigner.publishChangeSuccess', "The changes have been successfully published.")); saveNotificationHandle.updateMessage(localize('tableDesigner.publishChangeSuccess', "The changes have been successfully published."));
this.tableInfo = result.newTableInfo; this.tableInfo = result.newTableInfo;
this.updateState(true, false); this.updateState(true, false);
@@ -185,7 +190,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
return; return;
} }
const dialog = this._instantiationService.createInstance(TableDesignerPublishDialog); const dialog = this._instantiationService.createInstance(TableDesignerPublishDialog);
const result = await dialog.open(report); const result = await dialog.open(<any>report.report);
if (result === TableDesignerPublishDialogResult.GenerateScript) { if (result === TableDesignerPublishDialogResult.GenerateScript) {
await this.generateScript(); await this.generateScript();
} else if (result === TableDesignerPublishDialogResult.UpdateDatabase) { } else if (result === TableDesignerPublishDialogResult.UpdateDatabase) {
@@ -239,31 +244,35 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
this.updateState(true, this.tableInfo.isNewTable); this.updateState(true, this.tableInfo.isNewTable);
this._viewModel = designerInfo.viewModel; this._viewModel = designerInfo.viewModel;
this._originalViewModel = this.tableInfo.isNewTable ? undefined : deepClone(this._viewModel); this._originalViewModel = this.tableInfo.isNewTable ? undefined : deepClone(this._viewModel);
this.setDefaultData(); this.setDesignerView(designerInfo.view);
}
private setDesignerView(tableDesignerView: azdata.designers.TableDesignerView) {
const tabs = []; const tabs = [];
if (designerInfo.view.columnTableOptions?.showTable) { if (tableDesignerView.columnTableOptions?.showTable) {
tabs.push(this.getColumnsTab(designerInfo.view.columnTableOptions, designerInfo.columnTypes)); tabs.push(this.getColumnsTab(tableDesignerView.columnTableOptions));
} }
if (designerInfo.view.foreignKeyTableOptions?.showTable) { tabs.push(this.getPrimaryKeyTab(tableDesignerView));
tabs.push(this.getForeignKeysTab(designerInfo.view.foreignKeyTableOptions));
if (tableDesignerView.foreignKeyTableOptions?.showTable) {
tabs.push(this.getForeignKeysTab(tableDesignerView.foreignKeyTableOptions, tableDesignerView.foreignKeyColumnMappingTableOptions));
} }
if (designerInfo.view.checkConstraintTableOptions?.showTable) { if (tableDesignerView.checkConstraintTableOptions?.showTable) {
tabs.push(this.getCheckConstraintsTab(designerInfo.view.checkConstraintTableOptions)); tabs.push(this.getCheckConstraintsTab(tableDesignerView.checkConstraintTableOptions));
} }
if (designerInfo.view.indexTableOptions?.showTable) { if (tableDesignerView.indexTableOptions?.showTable) {
tabs.push(this.getIndexesTab(designerInfo.view.indexTableOptions, designerInfo.view.indexColumnSpecificationTableOptions)); tabs.push(this.getIndexesTab(tableDesignerView.indexTableOptions, tableDesignerView.indexColumnSpecificationTableOptions));
} }
if (designerInfo.view.additionalTabs) { if (tableDesignerView.additionalTabs) {
tabs.push(...designerInfo.view.additionalTabs); tabs.push(...tableDesignerView.additionalTabs);
} }
tabs.push(this.getGeneralTab(designerInfo)); tabs.push(this.getGeneralTab(tableDesignerView));
this._view = { this._view = {
components: [{ 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[] = [ const generalTabComponents: DesignerDataPropertyInfo[] = [
{ {
componentType: 'dropdown', componentType: 'dropdown',
@@ -287,7 +296,6 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
description: localize('designer.table.description.schema', "The schema that contains the table."), description: localize('designer.table.description.schema', "The schema that contains the table."),
componentProperties: <DropDownProperties>{ componentProperties: <DropDownProperties>{
title: localize('tableDesigner.schemaTitle', "Schema"), title: localize('tableDesigner.schemaTitle', "Schema"),
values: designerInfo.schemas
} }
}, { }, {
componentType: 'input', componentType: 'input',
@@ -299,8 +307,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
} }
]; ];
if (designerInfo.view.additionalTableProperties) { if (tableDesignerView.additionalTableProperties) {
generalTabComponents.push(...designerInfo.view.additionalTableProperties); generalTabComponents.push(...tableDesignerView.additionalTableProperties);
} }
return <DesignerTab>{ return <DesignerTab>{
@@ -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[] = [ 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"), description: localize('designer.column.description.dataType', "Displays the data type name for the column"),
componentProperties: { componentProperties: {
title: localize('tableDesigner.columnTypeTitle', "Type"), title: localize('tableDesigner.columnTypeTitle', "Type"),
width: 100, width: 100
values: columnTypes
} }
}, { }, {
componentType: 'input', componentType: 'input',
@@ -404,14 +411,15 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
canAddRows: options.canAddRows, canAddRows: options.canAddRows,
canRemoveRows: options.canRemoveRows, canRemoveRows: options.canRemoveRows,
removeRowConfirmationMessage: options.removeRowConfirmationMessage, 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[] = [ const foreignKeyColumnMappingProperties: DesignerDataPropertyInfo[] = [
{ {
@@ -476,13 +484,12 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
group: localize('tableDesigner.foreignKeyColumns', "Columns"), group: localize('tableDesigner.foreignKeyColumns', "Columns"),
componentProperties: <DesignerTableProperties>{ componentProperties: <DesignerTableProperties>{
ariaLabel: localize('tableDesigner.foreignKeyColumns', "Columns"), ariaLabel: localize('tableDesigner.foreignKeyColumns', "Columns"),
columns: [designers.ForeignKeyColumnMappingProperty.Column, designers.ForeignKeyColumnMappingProperty.ForeignColumn], columns: this.getTableDisplayProperties(columnMappingTableOptions, [designers.ForeignKeyColumnMappingProperty.Column, designers.ForeignKeyColumnMappingProperty.ForeignColumn]),
itemProperties: foreignKeyColumnMappingProperties, itemProperties: this.addAdditionalTableProperties(columnMappingTableOptions, foreignKeyColumnMappingProperties),
objectTypeDisplayName: '', canAddRows: columnMappingTableOptions.canAddRows,
canAddRows: options.canAddRows, canRemoveRows: columnMappingTableOptions.canRemoveRows,
canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: columnMappingTableOptions.removeRowConfirmationMessage,
removeRowConfirmationMessage: options.removeRowConfirmationMessage, labelForAddNewButton: columnMappingTableOptions.labelForAddNewButton ?? localize('tableDesigner.addNewColumnMapping', "New Column Mapping")
showRemoveRowConfirmation: options.showRemoveRowConfirmation
} }
} }
]; ];
@@ -502,13 +509,70 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
canAddRows: options.canAddRows, canAddRows: options.canAddRows,
canRemoveRows: options.canRemoveRows, canRemoveRows: options.canRemoveRows,
removeRowConfirmationMessage: options.removeRowConfirmationMessage, 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: <DesignerTableProperties>{
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 <DesignerTab>{
title: localize('tableDesigner.PrimaryKeyTabTitle', "Primary Key"),
components: tabComponents
};
}
private getCheckConstraintsTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { private getCheckConstraintsTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab {
const checkConstraintProperties: DesignerDataPropertyInfo[] = [ const checkConstraintProperties: DesignerDataPropertyInfo[] = [
{ {
@@ -545,7 +609,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
canAddRows: options.canAddRows, canAddRows: options.canAddRows,
canRemoveRows: options.canRemoveRows, canRemoveRows: options.canRemoveRows,
removeRowConfirmationMessage: options.removeRowConfirmationMessage, 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: '', objectTypeDisplayName: '',
canAddRows: columnSpecTableOptions.canAddRows, canAddRows: columnSpecTableOptions.canAddRows,
canRemoveRows: columnSpecTableOptions.canRemoveRows, canRemoveRows: columnSpecTableOptions.canRemoveRows,
removeRowConfirmationMessage: options.removeRowConfirmationMessage, removeRowConfirmationMessage: columnSpecTableOptions.removeRowConfirmationMessage,
showRemoveRowConfirmation: options.showRemoveRowConfirmation showRemoveRowConfirmation: columnSpecTableOptions.showRemoveRowConfirmation,
labelForAddNewButton: columnSpecTableOptions.labelForAddNewButton ?? localize('tableDesigner.addNewColumnToIndex', "Add Column")
} }
} }
]; ];
@@ -605,7 +671,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
canAddRows: options.canAddRows, canAddRows: options.canAddRows,
canRemoveRows: options.canRemoveRows, canRemoveRows: options.canRemoveRows,
removeRowConfirmationMessage: options.removeRowConfirmationMessage, 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; 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 { private createTelemetryInfo(): ITelemetryEventProperties {
let telemetryInfo = { let telemetryInfo = {
provider: this._provider.providerId, provider: this._provider.providerId,
@@ -645,6 +699,21 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
return telemetryInfo; 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 * 1. 'Add' scenario
a. ['propertyName1']. Example: add a column to the columns property: ['columns']. a. ['propertyName1']. Example: add a column to the columns property: ['columns'].