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

@@ -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;
}
}