Table Designer - Save Changes feature and Editor related features (#17335)

* table designer add/remove row support

* save changes and editor support

* address comments

* fix build error

* including missing change

* lower case request name
This commit is contained in:
Alan Ren
2021-10-11 15:09:25 -07:00
committed by GitHub
parent e5f50499ce
commit ce4459a7b2
23 changed files with 495 additions and 90 deletions

View File

@@ -1015,12 +1015,19 @@ declare module 'azdata' {
*/
getTableDesignerInfo(table: TableInfo): Thenable<TableDesignerInfo>;
/**
*
* Process the table change.
* @param table the table information
* @param data the object contains the state of the table designer
* @param tableChangeInfo the information about the change user made through the UI.
*/
processTableEdit(table: TableInfo, data: DesignerData, tableChangeInfo: DesignerEdit): Thenable<DesignerEditResult>;
/**
* Save the table
* @param table the table information
* @param data the object contains the state of the table designer
*/
saveTable(table: TableInfo, data: DesignerData): Thenable<void>;
}
/**
@@ -1047,6 +1054,10 @@ declare module 'azdata' {
* A boolean value indicates whether a new table is being designed.
*/
isNewTable: boolean;
/**
* If this is not a new table, the id will be set to uniquely identify a table.
*/
id?: string;
/**
* Extension can store additional information that the provider needs to uniquely identify a table.
*/
@@ -1095,7 +1106,8 @@ declare module 'azdata' {
DefaultValue = 'defaultValue',
Length = 'length',
Name = 'name',
Type = 'type'
Type = 'type',
IsPrimaryKey = 'isPrimaryKey'
}
/**
@@ -1229,7 +1241,7 @@ declare module 'azdata' {
/**
* the new value
*/
value: any;
value?: any;
}
/**

View File

@@ -23,6 +23,9 @@ import { TableCellEditorFactory } from 'sql/base/browser/ui/table/tableCellEdito
import { CheckBoxColumn } from 'sql/base/browser/ui/table/plugins/checkboxColumn.plugin';
import { DesignerTabPanelView } from 'sql/base/browser/ui/designer/designerTabPanelView';
import { DesignerPropertiesPane, PropertiesPaneObjectContext } from 'sql/base/browser/ui/designer/designerPropertiesPane';
import { Button, IButtonStyles } from 'sql/base/browser/ui/button/button';
import { ButtonColumn } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin';
import { Codicon } from 'vs/base/common/codicons';
export interface IDesignerStyle {
tabbedPanelStyles?: ITabbedPanelStyles;
@@ -30,6 +33,7 @@ export interface IDesignerStyle {
tableStyles?: ITableStyles;
selectBoxStyles?: ISelectBoxStyles;
checkboxStyles?: ICheckboxStyles;
buttonStyles?: IButtonStyles;
}
export type DesignerUIComponent = InputBox | Checkbox | Table<Slick.SlickData> | SelectBox;
@@ -38,7 +42,6 @@ export type CreateComponentFunc = (container: HTMLElement, component: DesignerDa
export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerData) => void;
export class Designer extends Disposable implements IThemable {
private _horizontalSplitViewContainer: HTMLElement;
private _verticalSplitViewContainer: HTMLElement;
private _tabbedPanelContainer: HTMLElement;
@@ -55,6 +58,7 @@ export class Designer extends Disposable implements IThemable {
private _input: DesignerComponentInput;
private _tableCellEditorFactory: TableCellEditorFactory;
private _propertiesPane: DesignerPropertiesPane;
private _buttons: Button[] = [];
constructor(private readonly _container: HTMLElement,
private readonly _contextViewProvider: IContextViewProvider) {
@@ -144,7 +148,7 @@ export class Designer extends Disposable implements IThemable {
this._editorContainer.appendChild(editor);
}
private styleComponent(component: TabbedPanel | InputBox | Checkbox | Table<Slick.SlickData> | SelectBox): void {
private styleComponent(component: TabbedPanel | InputBox | Checkbox | Table<Slick.SlickData> | SelectBox | Button): void {
if (component instanceof InputBox) {
component.style(this._styles.inputBoxStyles);
} else if (component instanceof Checkbox) {
@@ -153,10 +157,13 @@ export class Designer extends Disposable implements IThemable {
component.style(this._styles.tabbedPanelStyles);
} else if (component instanceof Table) {
component.style(this._styles.tableStyles);
} else if (component instanceof Button) {
component.style(this._styles.buttonStyles);
} else {
component.style(this._styles.selectBoxStyles);
}
}
public style(styles: IDesignerStyle): void {
this._styles = styles;
this._componentMap.forEach((value, key, map) => {
@@ -172,6 +179,10 @@ export class Designer extends Disposable implements IThemable {
this._horizontalSplitView.style({
separatorBorder: styles.selectBoxStyles.selectBorder
});
this._buttons.forEach((button) => {
this.styleComponent(button);
});
}
public layout(dimension: DOM.Dimension) {
@@ -206,19 +217,22 @@ export class Designer extends Disposable implements IThemable {
this._tabbedPanel.layout(new DOM.Dimension(this._tabbedPanelContainer.clientWidth, this._tabbedPanelContainer.clientHeight));
}
private async updateComponentValues(): Promise<void> {
private async updatePropertiesPane(newContext: PropertiesPaneObjectContext): Promise<void> {
const data = await this._input.getData();
// data[ScriptPropertyName] -- todo- set the script editor
this._componentMap.forEach((value) => {
this.setComponentValue(value.defintion, value.component, data);
});
let type: string;
let components: DesignerDataPropertyInfo[];
let inputData: DesignerData;
let context: PropertiesPaneObjectContext;
const currentContext = this._propertiesPane.context;
if (currentContext === 'root' || currentContext === undefined) {
if (newContext !== 'root') {
context = newContext;
const tableData = data[newContext.parentProperty] as DesignerTableProperties;
const tableProperties = this._componentMap.get(newContext.parentProperty).defintion.componentProperties as DesignerTableProperties;
inputData = tableData.data[newContext.index] as DesignerData;
components = tableProperties.itemProperties;
type = tableProperties.objectTypeDisplayName;
}
if (!inputData) {
context = 'root';
components = [];
this._componentMap.forEach(value => {
@@ -226,20 +240,25 @@ export class Designer extends Disposable implements IThemable {
});
type = this._input.objectTypeDisplayName;
inputData = data;
} else {
context = currentContext;
const tableData = data[currentContext.parentProperty] as DesignerTableProperties;
const tableProperties = this._componentMap.get(currentContext.parentProperty).defintion.componentProperties as DesignerTableProperties;
inputData = tableData.data[currentContext.index] as DesignerData;
components = tableProperties.itemProperties;
type = tableProperties.objectTypeDisplayName;
}
this._propertiesPane.show({
context: context,
type: type,
components: components,
data: inputData
if (inputData) {
this._propertiesPane.show({
context: context,
type: type,
components: components,
data: inputData
});
}
}
private async updateComponentValues(): Promise<void> {
const data = await this._input.getData();
// data[ScriptPropertyName] -- todo- set the script editor
this._componentMap.forEach((value) => {
this.setComponentValue(value.defintion, value.component, data);
});
await this.updatePropertiesPane(this._propertiesPane.context ?? 'root');
}
private async handleEdit(edit: DesignerEdit): Promise<void> {
@@ -251,6 +270,14 @@ export class Designer extends Disposable implements IThemable {
if (result.isValid) {
this._supressEditProcessing = true;
await this.updateComponentValues();
if (edit.type === DesignerEditType.Add) {
// Move focus to the first cell of the newly added row.
const data = await this._input.getData();
const propertyName = edit.property as string;
const tableData = data[propertyName] as DesignerTableProperties;
const table = this._componentMap.get(propertyName).component as Table<Slick.SlickData>;
table.setActiveCell(tableData.data.length - 1, 0);
}
this._supressEditProcessing = false;
} else {
//TODO: add error notification
@@ -319,9 +346,19 @@ export class Designer extends Disposable implements IThemable {
case 'table':
const table = component as Table<Slick.SlickData>;
const tableDataView = table.getData() as TableDataView<Slick.SlickData>;
const newData = (data[definition.propertyName] as DesignerTableProperties).data;
let activeCell: Slick.Cell;
if (table.container.contains(document.activeElement)) {
// Note down the current active cell if the focus is currently in the table
// After the table is refreshed, the focus will be restored.
activeCell = Object.assign({}, table.activeCell);
}
tableDataView.clear();
tableDataView.push((data[definition.propertyName] as DesignerTableProperties).data);
tableDataView.push(newData);
table.rerenderGrid();
if (activeCell && newData.length > activeCell.row) {
table.setActiveCell(activeCell.row, activeCell.cell);
}
break;
case 'checkbox':
const checkbox = component as Checkbox;
@@ -356,15 +393,13 @@ export class Designer extends Disposable implements IThemable {
}
private createComponent(container: HTMLElement, componentDefinition: DesignerDataPropertyInfo, editIdentifier: DesignerEditIdentifier, addToComponentMap: boolean, setWidth: boolean): DesignerUIComponent {
const componentContainerClass = componentDefinition.componentType === 'table' ? '.full-row' : '';
const labelContainer = container.appendChild(DOM.$(componentContainerClass));
labelContainer.appendChild(DOM.$('span.component-label')).innerText = (componentDefinition.componentType === 'checkbox' || componentDefinition.componentProperties?.title === undefined) ? '' : componentDefinition.componentProperties.title;
const componentDiv = container.appendChild(DOM.$(componentContainerClass));
let component: DesignerUIComponent;
switch (componentDefinition.componentType) {
case 'input':
container.appendChild(DOM.$('')).appendChild(DOM.$('span.component-label')).innerText = componentDefinition.componentProperties?.title ?? '';
const inputContainer = container.appendChild(DOM.$(''));
const inputProperties = componentDefinition.componentProperties as InputBoxProperties;
const input = new InputBox(componentDiv, this._contextViewProvider, {
const input = new InputBox(inputContainer, this._contextViewProvider, {
ariaLabel: inputProperties.title,
type: inputProperties.inputType,
});
@@ -377,9 +412,11 @@ export class Designer extends Disposable implements IThemable {
component = input;
break;
case 'dropdown':
container.appendChild(DOM.$('')).appendChild(DOM.$('span.component-label')).innerText = componentDefinition.componentProperties?.title ?? '';
const dropdownContainer = container.appendChild(DOM.$(''));
const dropdownProperties = componentDefinition.componentProperties as DropDownProperties;
const dropdown = new SelectBox(dropdownProperties.values as string[], undefined, this._contextViewProvider, undefined);
dropdown.render(componentDiv);
dropdown.render(dropdownContainer);
dropdown.selectElem.style.height = '25px';
dropdown.onDidSelect(async (e) => {
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: e.selected });
@@ -387,8 +424,10 @@ export class Designer extends Disposable implements IThemable {
component = dropdown;
break;
case 'checkbox':
container.appendChild(DOM.$('')); // label container place holder
const checkboxContainer = container.appendChild(DOM.$(''));
const checkboxProperties = componentDefinition.componentProperties as CheckBoxProperties;
const checkbox = new Checkbox(componentDiv, {
const checkbox = new Checkbox(checkboxContainer, {
label: checkboxProperties.title
});
checkbox.onChange(async (newValue) => {
@@ -398,17 +437,40 @@ export class Designer extends Disposable implements IThemable {
break;
case 'table':
const tableProperties = componentDefinition.componentProperties as DesignerTableProperties;
const table = new Table(componentDiv, {
const buttonContainer = container.appendChild(DOM.$('.full-row')).appendChild(DOM.$('.add-row-button-container'));
const addNewText = localize('designer.newRowText', "Add New");
const addRowButton = new Button(buttonContainer, {
title: addNewText,
secondary: true
});
addRowButton.onDidClick(async () => {
await this.handleEdit({
type: DesignerEditType.Add,
property: componentDefinition.propertyName,
});
});
this.styleComponent(addRowButton);
addRowButton.label = addNewText;
addRowButton.icon = {
id: `add-row-button new codicon`
};
this._buttons.push(addRowButton);
const tableContainer = container.appendChild(DOM.$('.full-row'));
const table = new Table(tableContainer, {
dataProvider: new TableDataView()
}, {
editable: true,
autoEdit: true,
dataItemColumnValueExtractor: (data: any, column: Slick.Column<Slick.SlickData>): string => {
return data[column.field].value;
if (column.field) {
return data[column.field].value;
} else {
return undefined;
}
}
}
);
table.columns = tableProperties.columns.map(propName => {
});
table.ariaLabel = tableProperties.ariaLabel;
const columns = tableProperties.columns.map(propName => {
const propertyDefinition = tableProperties.itemProperties.find(item => item.propertyName === propName);
switch (propertyDefinition.componentType) {
case 'checkbox':
@@ -448,20 +510,36 @@ export class Designer extends Disposable implements IThemable {
};
}
});
table.layout(new DOM.Dimension(container.clientWidth, container.clientHeight));
const deleteRowColumn = new ButtonColumn({
id: 'deleteRow',
iconCssClass: Codicon.trash.classNames,
title: localize('designer.removeRowText', "Remove"),
width: 20,
resizable: false,
isFontIcon: true
});
deleteRowColumn.onClick(async (e) => {
const data = await this._input.getData();
(data[componentDefinition.propertyName] as DesignerTableProperties).data.splice(e.row, 1);
await this.handleEdit({
type: DesignerEditType.Remove,
property: componentDefinition.propertyName,
value: e.item
});
});
table.registerPlugin(deleteRowColumn);
columns.push(deleteRowColumn.definition);
table.columns = columns;
table.grid.onBeforeEditCell.subscribe((e, data): boolean => {
return data.item[data.column.field].enabled !== false;
});
table.grid.onActiveCellChanged.subscribe((e, data) => {
this._propertiesPane.show({
context: {
table.grid.onActiveCellChanged.subscribe(async (e, data) => {
if (data.row !== undefined) {
await this.updatePropertiesPane({
parentProperty: componentDefinition.propertyName,
index: data.row
},
type: tableProperties.objectTypeDisplayName,
components: tableProperties.itemProperties,
data: table.getData().getItem(data.row)
});
});
}
});
component = table;
break;

View File

@@ -10,6 +10,10 @@ import { Disposable } from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import { CreateComponentFunc } from 'sql/base/browser/ui/designer/designer';
const ButtonHeight = 30;
const HorizontalPadding = 10;
const VerticalPadding = 20;
export class DesignerTabPanelView extends Disposable implements IPanelView {
private _componentsContainer: HTMLElement;
private _tables: Table<Slick.SlickData>[] = [];
@@ -30,7 +34,7 @@ export class DesignerTabPanelView extends Disposable implements IPanelView {
layout(dimension: DOM.Dimension): void {
this._tables.forEach(table => {
table.layout(new DOM.Dimension(dimension.width - 10, dimension.height - 20));
table.layout(new DOM.Dimension(dimension.width - HorizontalPadding, dimension.height - VerticalPadding - ButtonHeight));
});
}
}

View File

@@ -3,7 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
export interface DesignerComponentInput {
/**
* The event that is triggerd when the designer state changes.
*/
readonly onStateChange: Event<DesignerState>;
/**
* Gets the object type display name.
*/
@@ -24,6 +31,27 @@ export interface DesignerComponentInput {
* @param edit the information about the edit.
*/
processEdit(edit: DesignerEdit): Promise<DesignerEditResult>;
/**
* A boolean value indicating whether the current state is valid.
*/
readonly valid: boolean;
/**
* A boolean value indicating whether the current state is dirty.
*/
readonly dirty: boolean;
/**
* A boolean value indicating whether the changes are being saved.
*/
readonly saving: boolean;
}
export interface DesignerState {
valid: boolean;
dirty: boolean;
saving: boolean;
}
export const NameProperty = 'name';
@@ -115,7 +143,7 @@ export enum DesignerEditType {
export interface DesignerEdit {
type: DesignerEditType;
property: DesignerEditIdentifier;
value: any;
value?: any;
}
export type DesignerEditIdentifier = string | { parentProperty: string, index: number, property: string };

View File

@@ -59,7 +59,8 @@
.designer-component .components-grid {
display: grid;
grid-template-columns: max-content auto; /* label, component*/
/* grid-template-columns: column 1 is for label, column 2 is for component.*/
grid-template-columns: max-content auto;
grid-template-rows: max-content;
grid-gap: 5px;
padding: 5px;
@@ -68,10 +69,23 @@
}
.designer-component .components-grid .full-row {
grid-area: span 1 / span 2; /* spans 1 row and 2 columns*/
grid-area: span 1 / span 2;
}
.designer-component .monaco-table .slick-cell.editable {
padding: 0px;
border-width: 0px;
}
.designer-component .add-row-button-container {
display: flex;
flex-flow: row-reverse;
}
.designer-component .add-row-button-container .codicon.add-row-button {
width: fit-content;
background-repeat: no-repeat;
background-size: 13px;
padding-left: 17px;
background-position: 2px center;
}

View File

@@ -17,7 +17,7 @@ export interface ButtonColumnOptions extends IconColumnOptions {
/**
* Whether to show the text.
*/
showText?: boolean
showText?: boolean;
}
export class ButtonColumn<T extends Slick.SlickData> extends BaseClickableColumn<T> {
@@ -33,7 +33,10 @@ export class ButtonColumn<T extends Slick.SlickData> extends BaseClickableColumn
formatter: (row: number, cell: number, value: any, columnDef: Slick.Column<T>, dataContext: T): string => {
const iconValue = getIconCellValue(this.options, dataContext);
const escapedTitle = escape(iconValue.title ?? '');
const iconCssClasses = iconValue.iconCssClass ? `codicon icon slick-plugin-icon ${iconValue.iconCssClass}` : '';
let iconCssClasses = '';
if (iconValue.iconCssClass) {
iconCssClasses = this.options.isFontIcon ? iconValue.iconCssClass : `codicon icon slick-plugin-icon ${iconValue.iconCssClass}`;
}
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 `<button tabindex=-1 class="${iconCssClasses} ${buttonTypeCssClass}" title="${escapedTitle}" aria-label="${escapedTitle}">${buttonText}</button>`;

View File

@@ -140,6 +140,11 @@ export interface IconColumnOptions extends BaseTableColumnOptions {
* The title for all the cells. If the 'field' is provided, the cell values will overwrite this value.
*/
title?: string
/**
* Whether the icon is font icon. If true, no other class names will be auto appended.
*/
isFontIcon?: boolean;
}
export interface IconCellValue {

View File

@@ -426,4 +426,8 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
public set ariaLabel(value: string) {
this._tableContainer.setAttribute('aria-label', value);
}
public get container(): HTMLElement {
return this._tableContainer;
}
}

View File

@@ -8,7 +8,7 @@ import * as colors from './colors';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import * as cr from 'vs/platform/theme/common/colorRegistry';
import * as sqlcr from 'sql/platform/theme/common/colorRegistry';
import { attachStyler, computeStyles, defaultListStyles, IColorMapping, IStyleOverrides } from 'vs/platform/theme/common/styler';
import { attachStyler, computeStyles, defaultButtonStyles, defaultListStyles, IColorMapping, IStyleOverrides } from 'vs/platform/theme/common/styler';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IThemable } from 'vs/base/common/styler';
@@ -52,7 +52,7 @@ export interface IInputBoxStyleOverrides extends IStyleOverrides {
inputValidationErrorBackground?: cr.ColorIdentifier
}
export const defaultInputBoxStyleOverrides: IInputBoxStyleOverrides = {
export const defaultInputBoxStyles: IInputBoxStyleOverrides = {
inputBackground: cr.inputBackground,
inputForeground: cr.inputForeground,
disabledInputBackground: colors.disabledInputBackground,
@@ -67,7 +67,7 @@ export const defaultInputBoxStyleOverrides: IInputBoxStyleOverrides = {
};
export function attachInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: IInputBoxStyleOverrides): IDisposable {
return attachStyler(themeService, { ...defaultInputBoxStyleOverrides, ...(style || {}) }, widget);
return attachStyler(themeService, { ...defaultInputBoxStyles, ...(style || {}) }, widget);
}
export interface ISelectBoxStyleOverrides extends IStyleOverrides {
@@ -91,7 +91,7 @@ export interface ISelectBoxStyleOverrides extends IStyleOverrides {
listHoverForeground?: cr.ColorIdentifier
}
export const defaultSelectBoxStyleOverrides: ISelectBoxStyleOverrides = {
export const defaultSelectBoxStyles: ISelectBoxStyleOverrides = {
selectBackground: cr.selectBackground,
selectListBackground: cr.selectListBackground,
selectForeground: cr.selectForeground,
@@ -114,7 +114,7 @@ export const defaultSelectBoxStyleOverrides: ISelectBoxStyleOverrides = {
};
export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeService, style?: ISelectBoxStyleOverrides): IDisposable {
return attachStyler(themeService, { ...defaultSelectBoxStyleOverrides, ...(style || {}) }, widget);
return attachStyler(themeService, { ...defaultSelectBoxStyles, ...(style || {}) }, widget);
}
export function attachListBoxStyler(widget: IThemable, themeService: IThemeService, style?:
@@ -163,7 +163,7 @@ export interface ITableStyleOverrides extends IStyleOverrides {
tableHeaderForeground?: cr.ColorIdentifier,
}
export const defaultTableStyleOverrides: ITableStyleOverrides = {
export const defaultTableStyles: ITableStyleOverrides = {
listFocusBackground: cr.listFocusBackground,
listFocusForeground: cr.listFocusForeground,
listActiveSelectionBackground: cr.listActiveSelectionBackground,
@@ -185,7 +185,7 @@ export const defaultTableStyleOverrides: ITableStyleOverrides = {
};
export function attachTableStyler(widget: IThemable, themeService: IThemeService, style?: ITableStyleOverrides): IDisposable {
return attachStyler(themeService, { ...defaultTableStyleOverrides, ...(style || {}) }, widget);
return attachStyler(themeService, { ...defaultTableStyles, ...(style || {}) }, widget);
}
export interface IHighPerfTableStyleOverrides extends IStyleOverrides {
@@ -267,7 +267,7 @@ export interface IEditableDropdownStyleOverrides extends IStyleOverrides {
contextBorder?: cr.ColorIdentifier
}
export const defaultEditableDropdownStyleOverrides: IEditableDropdownStyleOverrides = {
export const defaultEditableDropdownStyle: IEditableDropdownStyleOverrides = {
listFocusBackground: cr.listFocusBackground,
listFocusForeground: cr.listFocusForeground,
listActiveSelectionBackground: cr.listActiveSelectionBackground,
@@ -299,14 +299,14 @@ export const defaultEditableDropdownStyleOverrides: IEditableDropdownStyleOverri
export function attachEditableDropdownStyler(widget: IThemable, themeService: IThemeService, style?: IEditableDropdownStyleOverrides): IDisposable {
return attachStyler(themeService, { ...defaultEditableDropdownStyleOverrides, ...(style || {}) }, widget);
return attachStyler(themeService, { ...defaultEditableDropdownStyle, ...(style || {}) }, widget);
}
export interface ICheckboxStyleOverrides extends IStyleOverrides {
disabledCheckboxForeground?: cr.ColorIdentifier
}
export const defaultCheckboxStyleOverrides: ICheckboxStyleOverrides = {
export const defaultCheckboxStyles: ICheckboxStyleOverrides = {
disabledCheckboxForeground: colors.disabledCheckboxForeground
};
@@ -352,7 +352,7 @@ export function attachInfoButtonStyler(widget: IThemable, themeService: IThemeSe
export function attachTableFilterStyler(widget: IThemable, themeService: IThemeService): IDisposable {
return attachStyler(themeService, {
...defaultInputBoxStyleOverrides,
...defaultInputBoxStyles,
buttonForeground: cr.buttonForeground,
buttonBackground: cr.buttonBackground,
buttonHoverBackground: cr.buttonHoverBackground,
@@ -374,15 +374,17 @@ export function attachTableFilterStyler(widget: IThemable, themeService: IThemeS
export function attachDesignerStyler(widget: any, themeService: IThemeService): IDisposable {
function applyStyles(): void {
const colorTheme = themeService.getColorTheme();
const inputStyles = computeStyles(colorTheme, defaultInputBoxStyleOverrides);
const selectBoxStyles = computeStyles(colorTheme, defaultSelectBoxStyleOverrides);
const tableStyles = computeStyles(colorTheme, defaultTableStyleOverrides);
const checkboxStyles = computeStyles(colorTheme, defaultCheckboxStyleOverrides);
const inputStyles = computeStyles(colorTheme, defaultInputBoxStyles);
const selectBoxStyles = computeStyles(colorTheme, defaultSelectBoxStyles);
const tableStyles = computeStyles(colorTheme, defaultTableStyles);
const checkboxStyles = computeStyles(colorTheme, defaultCheckboxStyles);
const buttonStyles = computeStyles(colorTheme, defaultButtonStyles);
widget.style({
inputBoxStyles: inputStyles,
selectBoxStyles: selectBoxStyles,
tableStyles: tableStyles,
checkboxStyles: checkboxStyles
checkboxStyles: checkboxStyles,
buttonStyles: buttonStyles
});
}

View File

@@ -512,11 +512,15 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData
$registerTableDesignerProvider(providerId: string, handle: number): Promise<any> {
const self = this;
this._tableDesignerService.registerProvider(providerId, <azdata.designers.TableDesignerProvider>{
providerId: providerId,
getTableDesignerInfo(tableInfo: azdata.designers.TableInfo): Thenable<azdata.designers.TableDesignerInfo> {
return self._proxy.$getTableDesignerInfo(handle, tableInfo);
},
processTableEdit(table, data, edit): Thenable<azdata.designers.DesignerEditResult> {
return self._proxy.$processTableDesignerEdit(handle, table, data, edit);
},
saveTable(tableInfo: azdata.designers.TableInfo, data: azdata.designers.DesignerData): Thenable<void> {
return self._proxy.$saveTable(handle, tableInfo, data);
}
});

View File

@@ -900,6 +900,10 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
return this._resolveProvider<azdata.designers.TableDesignerProvider>(handle).processTableEdit(table, data, edit);
}
public override $saveTable(handle, table: azdata.designers.TableInfo, data: azdata.designers.DesignerData): Thenable<void> {
return this._resolveProvider<azdata.designers.TableDesignerProvider>(handle).saveTable(table, data);
}
public override $openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo): Promise<void> {
this._proxy.$openTableDesigner(providerId, tableInfo);
return Promise.resolve();

View File

@@ -537,6 +537,11 @@ export abstract class ExtHostDataProtocolShape {
*/
$processTableDesignerEdit(handle, table: azdata.designers.TableInfo, data: azdata.designers.DesignerData, edit: azdata.designers.DesignerEdit): Thenable<azdata.designers.DesignerEditResult> { throw ni(); }
/**
* Process the table edit.
*/
$saveTable(handle, table: azdata.designers.TableInfo, data: azdata.designers.DesignerData): Thenable<void> { throw ni(); }
/**
* Open a new instance of table designer.
*/

View File

@@ -921,7 +921,8 @@ export namespace designers {
Type = 'type',
AllowNulls = 'allowNulls',
DefaultValue = 'defaultValue',
Length = 'length'
Length = 'length',
IsPrimaryKey = 'isPrimaryKey'
}
export enum DesignerEditType {

View File

@@ -9,16 +9,39 @@ import { URI } from 'vs/workbench/workbench.web.api';
import { TableDesignerComponentInput } from 'sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput';
import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface';
import * as azdata from 'azdata';
import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
const NewTable: string = localize('tableDesigner.newTable', "New Table");
export class TableDesignerInput extends EditorInput {
public static ID: string = 'workbench.editorinputs.tableDesignerInput';
private _designerComponentInput: TableDesignerComponentInput;
constructor(provider: TableDesignerProvider,
private _tableInfo: azdata.designers.TableInfo) {
private _name: string;
constructor(
private _provider: TableDesignerProvider,
private _tableInfo: azdata.designers.TableInfo,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IEditorService editorService: IEditorService) {
super();
this._designerComponentInput = new TableDesignerComponentInput(provider, this._tableInfo);
this._designerComponentInput = this._instantiationService.createInstance(TableDesignerComponentInput, this._provider, this._tableInfo);
this._register(this._designerComponentInput.onStateChange((e) => {
this._onDidChangeDirty.fire();
}));
const existingNames = editorService.editors.map(editor => editor.getName());
if (this._tableInfo.isNewTable) {
// Find the next available unique name for the new table designer
let idx = 1;
do {
this._name = `${this._tableInfo.server}.${this._tableInfo.database} - ${NewTable} ${idx}`;
idx++;
} while (existingNames.indexOf(this._name) !== -1);
} else {
this._name = `${this._tableInfo.server}.${this._tableInfo.database} - ${this._tableInfo.schema}.${this._tableInfo.name}`;
}
}
get typeId(): string {
@@ -34,7 +57,33 @@ export class TableDesignerInput extends EditorInput {
}
override getName(): string {
const tableName = this._tableInfo.isNewTable ? NewTable : `${this._tableInfo.schema}.${this._tableInfo.name}`;
return `${this._tableInfo.server}.${this._tableInfo.database} - ${tableName}`;
return this._name;
}
override isDirty(): boolean {
return this._designerComponentInput.dirty;
}
override isSaving(): boolean {
return this._designerComponentInput.saving;
}
override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
await this._designerComponentInput.save();
return this;
}
override async revert(group: GroupIdentifier, options?: IRevertOptions): Promise<void> {
await this._designerComponentInput.revert();
}
override matches(otherInput: any): boolean {
// For existing tables, the table designer provider will give us unique id, we can use it to do the match.
// For new tables, we can do the match using their names.
return otherInput instanceof TableDesignerInput
&& this._provider.providerId === otherInput._provider.providerId
&& this._tableInfo.isNewTable === otherInput._tableInfo.isNewTable
&& (!this._tableInfo.isNewTable || this.getName() === otherInput.getName())
&& (this._tableInfo.isNewTable || this._tableInfo.id === otherInput._tableInfo.id);
}
}

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TableDesignerComponentInput } from 'sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { IDisposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
export class SaveTableChangesAction extends Action {
public static ID = 'tableDesigner.saveTableChanges';
public static LABEL = localize('tableDesigner.saveTableChanges', "Save Changes");
private _input: TableDesignerComponentInput;
private _onStateChangeDisposable: IDisposable;
constructor(
) {
super(SaveTableChangesAction.ID, SaveTableChangesAction.LABEL, Codicon.save.classNames);
}
public setContext(input: TableDesignerComponentInput): void {
this._input = input;
this.updateState();
this._onStateChangeDisposable?.dispose();
this._onStateChangeDisposable = input.onStateChange((e) => {
this.updateState();
});
}
public override async run(): Promise<void> {
await this._input.save();
}
private updateState(): void {
this.enabled = this._input.dirty && this._input.valid && !this._input.saving;
}
override dispose() {
super.dispose();
this._onStateChangeDisposable?.dispose();
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.table-designer-main-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.table-designer-main-container .actionbar-container {
flex: 0 0 auto;
padding: 5px;
border-width: 0 0 1px 0;
border-style: solid;
border-bottom-color: rgba(128, 128, 128, 0.35);
}
.table-designer-main-container .designer-container {
flex: 1 1 auto;
}

View File

@@ -3,10 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/tableDesignerEditor';
import { Designer } from 'sql/base/browser/ui/designer/designer';
import { attachDesignerStyler } from 'sql/platform/theme/common/styler';
import { TableDesignerInput } from 'sql/workbench/browser/editor/tableDesigner/tableDesignerInput';
import * as DOM from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
@@ -15,17 +17,21 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { SaveTableChangesAction } from 'sql/workbench/contrib/tableDesigner/browser/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class TableDesignerEditor extends EditorPane {
public static readonly ID: string = 'workbench.editor.tableDesigner';
private _designer: Designer;
private _saveChangesAction: SaveTableChangesAction;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
@IStorageService storageService: IStorageService,
@IContextViewService private _contextViewService: IContextViewService
@IContextViewService private _contextViewService: IContextViewService,
@IInstantiationService private _instantiationService: IInstantiationService
) {
super(TableDesignerEditor.ID, telemetryService, themeService, storageService);
}
@@ -36,12 +42,22 @@ export class TableDesignerEditor extends EditorPane {
override async setInput(input: TableDesignerInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await super.setInput(input, options, context, token);
this._designer.setInput(input.getComponentInput());
const designerInput = input.getComponentInput();
this._designer.setInput(designerInput);
this._saveChangesAction.setContext(designerInput);
}
protected createEditor(parent: HTMLElement): void {
// The editor is only created once per editor group.
this._designer = new Designer(parent, this._contextViewService);
const container = parent.appendChild(DOM.$('.table-designer-main-container'));
const actionbarContainer = container.appendChild(DOM.$('.actionbar-container'));
const designerContainer = container.appendChild(DOM.$('.designer-container'));
const actionbar = new ActionBar(actionbarContainer);
this._register(actionbar);
this._saveChangesAction = this._instantiationService.createInstance(SaveTableChangesAction);
this._saveChangesAction.enabled = false;
actionbar.push(this._saveChangesAction, { icon: true, label: false });
this._designer = new Designer(designerContainer, this._contextViewService);
this._register(attachDesignerStyler(this._designer, this.themeService));
}

View File

@@ -4,18 +4,39 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { DesignerData, DesignerEdit, DesignerEditResult, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties } from 'sql/base/browser/ui/designer/interfaces';
import { DesignerData, DesignerEdit, DesignerEditResult, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerState } from 'sql/base/browser/ui/designer/interfaces';
import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface';
import { localize } from 'vs/nls';
import { designers } from 'sql/workbench/api/common/sqlExtHostTypes';
import { Emitter, Event } from 'vs/base/common/event';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
export class TableDesignerComponentInput implements DesignerComponentInput {
private _data: DesignerData;
private _view: DesignerView;
private _valid: boolean = true;
private _dirty: boolean = false;
private _saving: boolean = false;
private _onStateChange = new Emitter<DesignerState>();
public readonly onStateChange: Event<DesignerState> = this._onStateChange.event;
constructor(private readonly _provider: TableDesignerProvider,
private _tableInfo: azdata.designers.TableInfo) {
private _tableInfo: azdata.designers.TableInfo,
@INotificationService private readonly _notificationService: INotificationService) {
}
get valid(): boolean {
return this._valid;
}
get dirty(): boolean {
return this._dirty;
}
get saving(): boolean {
return this._saving;
}
get objectTypeDisplayName(): string {
@@ -41,12 +62,47 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
if (result.isValid) {
this._data = result.data;
}
this.updateState(result.isValid, true, this.saving);
return {
isValid: result.isValid,
errors: result.errors
};
}
async save(): Promise<void> {
const notificationHandle = this._notificationService.notify({
severity: Severity.Info,
message: localize('tableDesigner.savingChanges', "Saving table designer changes...")
});
try {
this.updateState(this.valid, this.dirty, true);
await this._provider.saveTable(this._tableInfo, this._data);
this.updateState(true, false, false);
notificationHandle.updateMessage(localize('tableDesigner.savedChangeSuccess', "The changes have been successfully saved."));
} catch (error) {
notificationHandle.updateSeverity(Severity.Error);
notificationHandle.updateMessage(localize('tableDesigner.saveChangeError', "An error occured while saving changes: {0}", error?.message ?? error));
this.updateState(this.valid, this.dirty, false);
}
}
async revert(): Promise<void> {
this.updateState(true, false, false);
}
private updateState(valid: boolean, dirty: boolean, saving: boolean): void {
if (this._dirty !== dirty || this._valid !== valid || this._saving !== saving) {
this._dirty = dirty;
this._valid = valid;
this._saving = saving;
this._onStateChange.fire({
valid: this._valid,
dirty: this._dirty,
saving: this._saving
});
}
}
private async initialize(): Promise<void> {
const designerInfo = await this._provider.getTableDesignerInfo(this._tableInfo);
@@ -115,6 +171,12 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
componentProperties: {
title: localize('tableDesigner.columnAllowNullTitle', "Allow Nulls"),
}
}, {
componentType: 'checkbox',
propertyName: designers.TableColumnProperty.IsPrimaryKey,
componentProperties: {
title: localize('tableDesigner.columnIsPrimaryKeyTitle', "Primary Key"),
}
}
];
@@ -135,7 +197,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
designers.TableColumnProperty.Type,
designers.TableColumnProperty.Length,
designers.TableColumnProperty.DefaultValue,
designers.TableColumnProperty.AllowNulls
designers.TableColumnProperty.AllowNulls,
designers.TableColumnProperty.IsPrimaryKey
],
itemProperties: columnProperties,
objectTypeDisplayName: localize('tableDesigner.columnTypeName', "Column")

View File

@@ -8,10 +8,12 @@ import { invalidProvider } from 'sql/base/common/errors';
import * as azdata from 'azdata';
import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { TableDesignerInput } from 'sql/workbench/browser/editor/tableDesigner/tableDesignerInput';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class TableDesignerService implements ITableDesignerService {
constructor(@IEditorService private _editorService: IEditorService) { }
constructor(@IEditorService private _editorService: IEditorService,
@IInstantiationService private _instantiationService: IInstantiationService) { }
public _serviceBrand: undefined;
private _providers = new Map<string, TableDesignerProvider>();
@@ -40,7 +42,7 @@ export class TableDesignerService implements ITableDesignerService {
public async openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo): Promise<void> {
const provider = this.getProvider(providerId);
const tableDesignerInput = new TableDesignerInput(provider, tableInfo);
const tableDesignerInput = this._instantiationService.createInstance(TableDesignerInput, provider, tableInfo);
await this._editorService.openEditor(tableDesignerInput, { pinned: true }, ACTIVE_GROUP);
}
}