move the designer component to workbench layer (#17620)

This commit is contained in:
Alan Ren
2021-11-08 10:26:27 -08:00
committed by GitHub
parent f91a228066
commit c00c5e044b
8 changed files with 24 additions and 25 deletions

View File

@@ -0,0 +1,757 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerEditIdentifier, DesignerViewModel, DesignerDataPropertyInfo,
DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties, DesignerComponentTypeName,
DesignerEditProcessedEventArgs, DesignerStateChangedEventArgs, DesignerAction, DesignerUIState, DesignerTextEditor, ScriptProperty
}
from 'sql/workbench/browser/designer/interfaces';
import { IPanelTab, ITabbedPanelStyles, TabbedPanel } from 'sql/base/browser/ui/panel/panel';
import * as DOM from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import 'vs/css!./media/designer';
import { ITableStyles } from 'sql/base/browser/ui/table/interfaces';
import { IThemable } from 'vs/base/common/styler';
import { Checkbox, ICheckboxStyles } from 'sql/base/browser/ui/checkbox/checkbox';
import { Table } from 'sql/base/browser/ui/table/table';
import { ISelectBoxStyles, SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { localize } from 'vs/nls';
import { TableCellEditorFactory } from 'sql/base/browser/ui/table/tableCellEditorFactory';
import { CheckBoxColumn } from 'sql/base/browser/ui/table/plugins/checkboxColumn.plugin';
import { DesignerTabPanelView } from 'sql/workbench/browser/designer/designerTabPanelView';
import { DesignerPropertiesPane, PropertiesPaneObjectContext } from 'sql/workbench/browser/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';
import { Color } from 'vs/base/common/color';
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { DesignerScriptEditor } from 'sql/workbench/browser/designer/designerScriptEditor';
export interface IDesignerStyle {
tabbedPanelStyles?: ITabbedPanelStyles;
inputBoxStyles?: IInputBoxStyles;
tableStyles?: ITableStyles;
selectBoxStyles?: ISelectBoxStyles;
checkboxStyles?: ICheckboxStyles;
buttonStyles?: IButtonStyles;
paneSeparator?: Color;
groupHeaderBackground?: Color;
}
export type DesignerUIComponent = InputBox | Checkbox | Table<Slick.SlickData> | SelectBox;
export type CreateComponentsFunc = (container: HTMLElement, components: DesignerDataPropertyInfo[], editIdentifierGetter: (property: DesignerDataPropertyInfo) => DesignerEditIdentifier) => DesignerUIComponent[];
export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void;
export class Designer extends Disposable implements IThemable {
private _loadingSpinner: LoadingSpinner;
private _horizontalSplitViewContainer: HTMLElement;
private _verticalSplitViewContainer: HTMLElement;
private _tabbedPanelContainer: HTMLElement;
private _editorContainer: HTMLElement;
private _horizontalSplitView: SplitView;
private _verticalSplitView: SplitView;
private _tabbedPanel: TabbedPanel;
private _contentContainer: HTMLElement;
private _topContentContainer: HTMLElement;
private _propertiesPaneContainer: HTMLElement;
private _styles: IDesignerStyle = {};
private _supressEditProcessing: boolean = false;
private _componentMap = new Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>();
private _input: DesignerComponentInput;
private _tableCellEditorFactory: TableCellEditorFactory;
private _propertiesPane: DesignerPropertiesPane;
private _buttons: Button[] = [];
private _inputDisposable: DisposableStore;
private _loadingTimeoutHandle: any;
private _groupHeaders: HTMLElement[] = [];
private _textEditor: DesignerTextEditor;
constructor(private readonly _container: HTMLElement,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IContextViewService private readonly _contextViewProvider: IContextViewService) {
super();
this._tableCellEditorFactory = new TableCellEditorFactory(
{
valueGetter: (item, column): string => {
return item[column.field].value;
},
valueSetter: (context: string, row: number, item: DesignerTableComponentRowData, column: Slick.Column<Slick.SlickData>, value: string): void => {
this.handleEdit({
type: DesignerEditType.Update,
property: {
parentProperty: context,
index: row,
property: column.field
},
value: value
});
},
optionsGetter: (item, column): string[] => {
return item[column.field].options;
},
editorStyler: (component) => {
this.styleComponent(component);
}
}, this._contextViewProvider
);
this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true });
this._verticalSplitViewContainer = DOM.$('.designer-component');
this._horizontalSplitViewContainer = DOM.$('.container');
this._contentContainer = DOM.$('.content-container');
this._topContentContainer = DOM.$('.top-content-container.components-grid');
this._tabbedPanelContainer = DOM.$('.tabbed-panel-container');
this._editorContainer = DOM.$('.editor-container');
this._propertiesPaneContainer = DOM.$('.properties-container');
this._verticalSplitView = new SplitView(this._verticalSplitViewContainer, { orientation: Orientation.VERTICAL });
this._horizontalSplitView = new SplitView(this._horizontalSplitViewContainer, { orientation: Orientation.HORIZONTAL });
this._tabbedPanel = new TabbedPanel(this._tabbedPanelContainer);
this._container.appendChild(this._verticalSplitViewContainer);
this._contentContainer.appendChild(this._topContentContainer);
this._contentContainer.appendChild(this._tabbedPanelContainer);
this._verticalSplitView.addView({
element: this._horizontalSplitViewContainer,
layout: size => {
this.layoutTabbedPanel();
},
minimumSize: 200,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._textEditor = this._instantiationService.createInstance(DesignerScriptEditor, this._editorContainer);
this._verticalSplitView.addView({
element: this._editorContainer,
layout: size => {
this._textEditor.layout(new DOM.Dimension(this._editorContainer.clientWidth, size));
},
minimumSize: 100,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._horizontalSplitView.addView({
element: this._contentContainer,
layout: size => {
this.layoutTabbedPanel();
},
minimumSize: 400,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._horizontalSplitView.addView({
element: this._propertiesPaneContainer,
layout: size => { },
minimumSize: 200,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._propertiesPane = new DesignerPropertiesPane(this._propertiesPaneContainer, (container, components, identifierGetter) => {
return this.createComponents(container, components, this._propertiesPane.componentMap, this._propertiesPane.groupHeaders, identifierGetter, false, true);
}, (definition, component, viewModel) => {
this.setComponentValue(definition, component, viewModel);
});
}
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) {
component.style(this._styles.checkboxStyles);
} else if (component instanceof TabbedPanel) {
component.style(this._styles.tabbedPanelStyles);
} else if (component instanceof Table) {
component.style(this._styles.tableStyles);
} else if (component instanceof Button) {
component.style(this._styles.buttonStyles);
} else {
component.style(this._styles.selectBoxStyles);
}
}
private styleGroupHeader(header: HTMLElement): void {
if (this._styles.groupHeaderBackground) {
header.style.backgroundColor = this._styles.groupHeaderBackground.toString();
}
}
public style(styles: IDesignerStyle): void {
this._styles = styles;
this._componentMap.forEach((value, key, map) => {
if (value.component.style) {
this.styleComponent(value.component);
}
});
this._propertiesPane.componentMap.forEach((value) => {
this.styleComponent(value.component);
});
this._verticalSplitView.style({
separatorBorder: styles.paneSeparator
});
this._horizontalSplitView.style({
separatorBorder: styles.paneSeparator
});
this._buttons.forEach((button) => {
this.styleComponent(button);
});
this._groupHeaders.forEach((header) => {
this.styleGroupHeader(header);
});
this._propertiesPane.groupHeaders.forEach((header) => {
this.styleGroupHeader(header);
});
}
public layout(dimension: DOM.Dimension) {
this._verticalSplitView.layout(dimension.height);
this._horizontalSplitView.layout(dimension.width);
}
public setInput(input: DesignerComponentInput): void {
// Save state
if (this._input) {
this._input.designerUIState = this.getUIState();
}
// Clean up
if (this._loadingTimeoutHandle) {
this.stopLoading();
}
this._buttons = [];
this._componentMap.clear();
DOM.clearNode(this._topContentContainer);
this._tabbedPanel.clearTabs();
this._propertiesPane.clear();
this._inputDisposable?.dispose();
this._groupHeaders = [];
// Initialize with new input
this._input = input;
this._inputDisposable = new DisposableStore();
this._inputDisposable.add(this._input.onInitialized(() => {
this.initializeDesigner();
}));
this._inputDisposable.add(this._input.onEditProcessed((args) => {
this.handleEditProcessedEvent(args);
}));
this._inputDisposable.add(this._input.onStateChange((args) => {
this.handleInputStateChangedEvent(args);
}));
if (this._input.view === undefined) {
this._input.initialize();
} else {
this.initializeDesigner();
}
if (this._input.pendingAction) {
this.updateLoadingStatus(this._input.pendingAction, true, false);
}
}
public override dispose(): void {
super.dispose();
this._inputDisposable?.dispose();
}
private initializeDesigner(): void {
const view = this._input.view;
if (view.components) {
this.createComponents(this._topContentContainer, view.components, this._componentMap, this._groupHeaders, component => component.propertyName, true, false);
}
view.tabs.forEach(tab => {
this._tabbedPanel.pushTab(this.createTabView(tab));
});
this.layoutTabbedPanel();
this.updateComponentValues();
this.restoreUIState();
}
private handleEditProcessedEvent(args: DesignerEditProcessedEventArgs): void {
const edit = args.edit;
const result = args.result;
if (result.isValid) {
this._supressEditProcessing = true;
this.updateComponentValues();
if (edit.type === DesignerEditType.Add) {
// Move focus to the first cell of the newly added row.
const propertyName = edit.property as string;
const tableData = this._input.viewModel[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
}
}
private handleInputStateChangedEvent(args: DesignerStateChangedEventArgs): void {
if (args.previousState.pendingAction !== args.currentState.pendingAction) {
const showLoading = args.currentState.pendingAction !== undefined;
const action = args.currentState.pendingAction || args.previousState.pendingAction;
this.updateLoadingStatus(action, showLoading, true);
}
}
private updateLoadingStatus(action: DesignerAction, showLoading: boolean, useDelay: boolean): void {
let message;
let timeout;
switch (action) {
case 'save':
message = showLoading ? localize('designer.savingChanges', "Saving changes...") : localize('designer.savingChangesCompleted', "Changes have been saved");
timeout = 0;
break;
case 'initialize':
message = showLoading ? localize('designer.loadingDesigner', "Loading designer...") : localize('designer.loadingDesignerCompleted', "Designer is loaded");
timeout = 0;
break;
case 'processEdit':
message = showLoading ? localize('designer.processingChanges', "Processing changes...") : localize('designer.processingChangesCompleted', "Changes have been processed");
// To make the edit experience smoother, only show the loading indicator if the request is not returning in 500ms.
timeout = 500;
break;
default:
return;
}
if (showLoading) {
this.startLoading(message, useDelay ? timeout : 0);
} else {
this.stopLoading(message);
}
}
private layoutTabbedPanel() {
this._tabbedPanel.layout(new DOM.Dimension(this._tabbedPanelContainer.clientWidth, this._tabbedPanelContainer.clientHeight));
}
private updatePropertiesPane(newContext: PropertiesPaneObjectContext): void {
const viewModel = this._input.viewModel;
let type: string;
let components: DesignerDataPropertyInfo[];
let inputViewModel: DesignerViewModel;
let context: PropertiesPaneObjectContext;
if (newContext !== 'root') {
context = newContext;
const tableData = viewModel[newContext.parentProperty] as DesignerTableProperties;
const tableProperties = this._componentMap.get(newContext.parentProperty).defintion.componentProperties as DesignerTableProperties;
inputViewModel = tableData.data[newContext.index] as DesignerViewModel;
components = tableProperties.itemProperties;
type = tableProperties.objectTypeDisplayName;
}
if (!inputViewModel) {
context = 'root';
components = [];
this._componentMap.forEach(value => {
components.push(value.defintion);
});
type = this._input.objectTypeDisplayName;
inputViewModel = viewModel;
}
if (inputViewModel) {
this._propertiesPane.show({
context: context,
type: type,
components: components,
viewModel: inputViewModel
});
}
}
private updateComponentValues(): void {
const viewModel = this._input.viewModel;
const scriptProperty = viewModel[ScriptProperty] as InputBoxProperties;
if (scriptProperty) {
this._textEditor.content = scriptProperty.value || '';
}
this._componentMap.forEach((value) => {
this.setComponentValue(value.defintion, value.component, viewModel);
});
this.updatePropertiesPane(this._propertiesPane.context ?? 'root');
}
private handleEdit(edit: DesignerEdit): void {
if (this._supressEditProcessing) {
return;
}
this.applyEdit(edit);
this._input.processEdit(edit);
}
private applyEdit(edit: DesignerEdit): void {
const viewModel = this._input.viewModel;
switch (edit.type) {
case DesignerEditType.Update:
if (typeof edit.property === 'string') {
// if the type of the property is string then the property is a top level property
if (!viewModel[edit.property]) {
viewModel[edit.property] = {};
}
const componentData = viewModel[edit.property];
const componentType = this._componentMap.get(edit.property).defintion.componentType;
this.setComponentData(componentType, componentData, edit.value);
} else {
const columnPropertyName = edit.property.property;
const tableInfo = this._componentMap.get(edit.property.parentProperty).defintion.componentProperties as DesignerTableProperties;
const tableProperties = viewModel[edit.property.parentProperty] as DesignerTableProperties;
if (!tableProperties.data[edit.property.index][columnPropertyName]) {
tableProperties.data[edit.property.index][columnPropertyName] = {};
}
const componentData = tableProperties.data[edit.property.index][columnPropertyName];
const itemProperty = tableInfo.itemProperties.find(property => property.propertyName === columnPropertyName);
if (itemProperty) {
this.setComponentData(itemProperty.componentType, componentData, edit.value);
}
}
break;
default:
break;
}
}
private setComponentData(componentType: DesignerComponentTypeName, componentData: any, value: any): void {
switch (componentType) {
case 'checkbox':
(<CheckBoxProperties>componentData).checked = value;
break;
case 'dropdown':
(<DropDownProperties>componentData).value = value;
break;
case 'input':
(<InputBoxProperties>componentData).value = value;
break;
}
}
private createTabView(tab: DesignerTab): IPanelTab {
const view = new DesignerTabPanelView(tab, (container, components, identifierGetter) => {
return this.createComponents(container, components, this._componentMap, this._groupHeaders, identifierGetter, true, false);
});
return {
identifier: tab.title,
title: tab.title,
view: view
};
}
private setComponentValue(definition: DesignerDataPropertyInfo, component: DesignerUIComponent, viewModel: DesignerViewModel): void {
// Skip the property if it is not in the data model
if (!viewModel[definition.propertyName]) {
return;
}
this._supressEditProcessing = true;
switch (definition.componentType) {
case 'input':
const input = component as InputBox;
const inputData = viewModel[definition.propertyName] as InputBoxProperties;
input.setEnabled(inputData.enabled ?? true);
input.value = inputData.value?.toString() ?? '';
break;
case 'table':
const table = component as Table<Slick.SlickData>;
const tableDataView = table.getData() as TableDataView<Slick.SlickData>;
const newData = (viewModel[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(newData);
table.rerenderGrid();
if (activeCell && newData.length > activeCell.row) {
table.setActiveCell(activeCell.row, activeCell.cell);
}
break;
case 'checkbox':
const checkbox = component as Checkbox;
const checkboxData = viewModel[definition.propertyName] as CheckBoxProperties;
if (checkboxData.enabled === false) {
checkbox.disable();
} else {
checkbox.enable();
}
checkbox.checked = checkboxData.checked;
break;
case 'dropdown':
const dropdown = component as SelectBox;
const defaultDropdownData = definition.componentProperties as DropDownProperties;
const dropdownData = viewModel[definition.propertyName] as DropDownProperties;
if (dropdownData.enabled === false) {
dropdown.disable();
} else {
dropdown.enable();
}
const options = (dropdownData.values || defaultDropdownData.values || []) as string[];
dropdown.setOptions(options);
const idx = options?.indexOf(dropdownData.value as string);
if (idx > -1) {
dropdown.select(idx);
}
break;
default:
break;
}
this._supressEditProcessing = false;
}
private createComponents(container: HTMLElement,
components: DesignerDataPropertyInfo[],
componentMap: Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>,
groupHeaders: HTMLElement[],
identifierGetter: (definition: DesignerDataPropertyInfo) => DesignerEditIdentifier,
setWidth: boolean, skipTableCreation: boolean = false): DesignerUIComponent[] {
const uiComponents = [];
const groupNames = [];
const componentsToCreate = skipTableCreation ? components.filter(component => component.componentType !== 'table') : components;
componentsToCreate.forEach(component => {
if (groupNames.indexOf(component.group) === -1) {
groupNames.push(component.group);
}
});
// only show groups when there are multiple of them.
if (groupNames.length < 2) {
componentsToCreate.forEach(component => {
uiComponents.push(this.createComponent(container, component, identifierGetter(component), componentMap, setWidth));
});
} else {
groupNames.forEach(group => {
const groupHeader = container.appendChild(DOM.$('div.full-row.group-header'));
groupHeaders.push(groupHeader);
this.styleGroupHeader(groupHeader);
groupHeader.innerText = group ?? localize('designer.generalGroupName', "General");
componentsToCreate.forEach(component => {
if (component.group === group) {
uiComponents.push(this.createComponent(container, component, identifierGetter(component), componentMap, setWidth));
}
});
});
}
return uiComponents;
}
private createComponent(container: HTMLElement,
componentDefinition: DesignerDataPropertyInfo,
editIdentifier: DesignerEditIdentifier,
componentMap: Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>,
setWidth: boolean): DesignerUIComponent {
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(inputContainer, this._contextViewProvider, {
ariaLabel: inputProperties.title,
type: inputProperties.inputType,
});
input.onLoseFocus((args) => {
if (args.hasChanged) {
this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: args.value });
}
});
if (setWidth && inputProperties.width !== undefined) {
input.width = inputProperties.width as number;
}
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(dropdownContainer);
dropdown.selectElem.style.height = '25px';
dropdown.onDidSelect((e) => {
this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: e.selected });
});
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(checkboxContainer, {
label: checkboxProperties.title
});
checkbox.onChange((newValue) => {
this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue });
});
component = checkbox;
break;
case 'table':
const tableProperties = componentDefinition.componentProperties as DesignerTableProperties;
if (tableProperties.canAddRows !== false) {
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(() => {
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 => {
if (column.field) {
return data[column.field].value;
} else {
return undefined;
}
}
});
table.ariaLabel = tableProperties.ariaLabel;
const columns = tableProperties.columns.map(propName => {
const propertyDefinition = tableProperties.itemProperties.find(item => item.propertyName === propName);
switch (propertyDefinition.componentType) {
case 'checkbox':
const checkboxColumn = new CheckBoxColumn({
field: propertyDefinition.propertyName,
name: propertyDefinition.componentProperties.title,
width: propertyDefinition.componentProperties.width as number
});
table.registerPlugin(checkboxColumn);
checkboxColumn.onChange((e) => {
this.handleEdit({
type: DesignerEditType.Update,
property: {
parentProperty: componentDefinition.propertyName,
index: e.row,
property: propertyDefinition.propertyName
},
value: e.value
});
});
return checkboxColumn.definition;
case 'dropdown':
const dropdownProperties = propertyDefinition.componentProperties as DropDownProperties;
return {
name: dropdownProperties.title,
field: propertyDefinition.propertyName,
editor: this._tableCellEditorFactory.getSelectBoxEditorClass(componentDefinition.propertyName, dropdownProperties.values as string[]),
width: dropdownProperties.width as number
};
default:
const inputProperties = propertyDefinition.componentProperties as InputBoxProperties;
return {
name: inputProperties.title,
field: propertyDefinition.propertyName,
editor: this._tableCellEditorFactory.getTextEditorClass(componentDefinition.propertyName, inputProperties.inputType),
width: inputProperties.width as number
};
}
});
if (tableProperties.canRemoveRows !== false) {
const deleteRowColumn = new ButtonColumn({
id: 'deleteRow',
iconCssClass: Codicon.trash.classNames,
title: localize('designer.removeRowText', "Remove"),
width: 20,
resizable: false,
isFontIcon: true
});
deleteRowColumn.onClick((e) => {
(this._input.viewModel[componentDefinition.propertyName] as DesignerTableProperties).data.splice(e.row, 1);
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) => {
if (data.row !== undefined) {
this.updatePropertiesPane({
parentProperty: componentDefinition.propertyName,
index: data.row
});
}
});
component = table;
break;
default:
throw new Error(localize('tableDesigner.unknownComponentType', "The component type: {0} is not supported", componentDefinition.componentType));
}
componentMap.set(componentDefinition.propertyName, {
defintion: componentDefinition,
component: component
});
this.styleComponent(component);
return component;
}
private startLoading(message: string, timeout: number): void {
this._loadingTimeoutHandle = setTimeout(() => {
this._loadingSpinner.loadingMessage = message;
this._loadingSpinner.loading = true;
if (this._container.contains(this._verticalSplitViewContainer)) {
this._container.removeChild(this._verticalSplitViewContainer);
}
}, timeout);
}
private stopLoading(message: string = ''): void {
clearTimeout(this._loadingTimeoutHandle);
this._loadingTimeoutHandle = undefined;
if (this._loadingSpinner.loading) {
this._loadingSpinner.loadingCompletedMessage = message;
this._loadingSpinner.loading = false;
if (!this._container.contains(this._verticalSplitViewContainer)) {
this._container.appendChild(this._verticalSplitViewContainer);
}
}
}
private getUIState(): DesignerUIState {
return {
activeTabId: this._tabbedPanel.activeTabId
};
}
private restoreUIState(): void {
if (this._input.designerUIState) {
this._tabbedPanel.showTab(this._input.designerUIState.activeTabId);
}
}
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CreateComponentsFunc, DesignerUIComponent, SetComponentValueFunc } from 'sql/workbench/browser/designer/designer';
import { DesignerViewModel, DesignerDataPropertyInfo } from 'sql/workbench/browser/designer/interfaces';
import * as DOM from 'vs/base/browser/dom';
import { equals } from 'vs/base/common/objects';
import { localize } from 'vs/nls';
export type PropertiesPaneObjectContext = 'root' | {
parentProperty: string;
index: number;
};
export interface ObjectInfo {
context: PropertiesPaneObjectContext;
type: string;
components: DesignerDataPropertyInfo[];
viewModel: DesignerViewModel;
}
export class DesignerPropertiesPane {
private _titleElement: HTMLElement;
private _contentElement: HTMLElement;
private _currentContext?: PropertiesPaneObjectContext;
private _componentMap = new Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>();
private _groupHeaders: HTMLElement[] = [];
constructor(container: HTMLElement, private _createComponents: CreateComponentsFunc, private _setComponentValue: SetComponentValueFunc) {
const titleContainer = container.appendChild(DOM.$('.title-container'));
this._titleElement = titleContainer.appendChild(DOM.$('div'));
this._contentElement = container.appendChild(DOM.$('.properties-content.components-grid'));
this._titleElement.innerText = localize('tableDesigner.propertiesPaneTitle', "Properties");
}
public get groupHeaders(): HTMLElement[] {
return this._groupHeaders;
}
public get componentMap(): Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }> {
return this._componentMap;
}
public get context(): PropertiesPaneObjectContext | undefined {
return this._currentContext;
}
public clear(): void {
this._componentMap.forEach((value) => {
value.component.dispose();
});
this._componentMap.clear();
this._groupHeaders = [];
DOM.clearNode(this._contentElement);
this._currentContext = undefined;
}
public show(item: ObjectInfo): void {
if (!equals(item.context, this._currentContext)) {
this.clear();
this._currentContext = item.context;
this._createComponents(this._contentElement, item.components, (property) => {
return this._currentContext === 'root' ? property.propertyName : {
parentProperty: this._currentContext.parentProperty,
index: this._currentContext.index,
property: property.propertyName
};
});
}
this._titleElement.innerText = localize({
key: 'tableDesigner.propertiesPaneTitleWithContext',
comment: ['{0} is the place holder for object type']
}, "{0} Properties", item.type);
this._componentMap.forEach((value) => {
this._setComponentValue(value.defintion, value.component, item.viewModel);
});
}
}

View File

@@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DesignerTextEditor } from 'sql/workbench/browser/designer/interfaces';
import { Event, Emitter } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput';
import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import * as nls from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { onUnexpectedError } from 'vs/base/common/errors';
class DesignerCodeEditor extends CodeEditorWidget {
}
export class DesignerScriptEditor extends BaseTextEditor implements DesignerTextEditor {
private _content: string;
private _contentChangeEventEmitter: Emitter<string> = new Emitter<string>();
readonly onDidContentChange: Event<string> = this._contentChangeEventEmitter.event;
private _untitledTextEditorModel: UntitledTextEditorModel;
private _editorInput: UntitledTextEditorInput;
private _editorModel: ITextModel;
public static ID = 'designer.editors.textEditor';
constructor(
private _container: HTMLElement,
@IModelService private _modelService: IModelService,
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,
@IThemeService themeService: IThemeService,
@IEditorService editorService: IEditorService,
@IEditorGroupsService editorGroupService: IEditorGroupsService
) {
super(DesignerScriptEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService);
this.create(this._container);
this.setVisible(true);
this._untitledTextEditorModel = this.instantiationService.createInstance(UntitledTextEditorModel, URI.from({ scheme: Schemas.untitled }), false, undefined, 'sql', undefined);
this._editorInput = this.instantiationService.createInstance(UntitledTextEditorInput, this._untitledTextEditorModel);
this.setInput(this._editorInput, undefined, undefined).catch(onUnexpectedError);
this._editorInput.resolve().then((model) => {
this._editorModel = model.textEditorModel;
});
}
public override createEditorControl(parent: HTMLElement, configuration: IEditorOptions): editorCommon.IEditor {
return this.instantiationService.createInstance(DesignerCodeEditor, parent, configuration, {});
}
protected override getConfigurationOverrides(): IEditorOptions {
const options = super.getConfigurationOverrides();
options.readOnly = true;
if (this.input) {
options.inDiffEditor = false;
options.scrollBeyondLastLine = false;
options.folding = false;
options.renderWhitespace = 'all';
options.wordWrap = 'off';
options.renderIndentGuides = false;
options.rulers = [];
options.glyphMargin = true;
options.minimap = {
enabled: true
};
}
return options;
}
override async setInput(input: UntitledTextEditorInput, options: ITextEditorOptions, context: IEditorOpenContext): Promise<void> {
await super.setInput(input, options, context, CancellationToken.None);
const editorModel = await this.input.resolve() as TextResourceEditorModel;
await editorModel.resolve();
this.getControl().setModel(editorModel.textEditorModel);
}
protected getAriaLabel(): string {
return nls.localize('designer.textEditorAriaLabel', "Designer text editor.");
}
public override layout(dimension: DOM.Dimension) {
this.getControl().layout(dimension);
}
get content(): string {
return this._content;
}
set content(val: string) {
this._content = val;
this._modelService.updateModel(this._editorModel, this._content);
this._untitledTextEditorModel.setDirty(false);
this.layout(new DOM.Dimension(this._container.clientWidth, this._container.clientHeight));
}
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DesignerTab } from 'sql/workbench/browser/designer/interfaces';
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
import { Table } from 'sql/base/browser/ui/table/table';
import { Disposable } from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import { CreateComponentsFunc } from 'sql/workbench/browser/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>[] = [];
constructor(private readonly _tab: DesignerTab, private _createComponents: CreateComponentsFunc) {
super();
this._componentsContainer = DOM.$('.components-grid');
const uiComponents = this._createComponents(this._componentsContainer, this._tab.components, component => component.propertyName);
uiComponents.forEach(component => {
if (component instanceof Table) {
this._tables.push(component);
}
});
}
render(container: HTMLElement): void {
container.appendChild(this._componentsContainer);
}
layout(dimension: DOM.Dimension): void {
this._tables.forEach(table => {
table.layout(new DOM.Dimension(dimension.width - HorizontalPadding, dimension.height - VerticalPadding - ButtonHeight));
});
}
}

View File

@@ -0,0 +1,221 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel';
import { Dimension } from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
export interface DesignerComponentInput {
/**
* The event that is triggerd when the designer state changes.
*/
readonly onStateChange: Event<DesignerStateChangedEventArgs>;
/**
* The event that is triggerd when the designer information is loaded.
*/
readonly onInitialized: Event<void>;
/**
* The event that is triggerd when an edit is processed.
*/
readonly onEditProcessed: Event<DesignerEditProcessedEventArgs>;
/**
* Gets the object type display name.
*/
readonly objectTypeDisplayName: string;
/**
* Gets the designer view specification.
*/
readonly view: DesignerView;
/**
* Gets the view model.
*/
readonly viewModel: DesignerViewModel;
/**
* Start initilizing the designer input object.
*/
initialize(): void;
/**
* Start processing the edit made in the designer, the OnEditProcessed event will be fired when the processing is done.
* @param edit the information about the edit.
*/
processEdit(edit: DesignerEdit): void;
/**
* 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;
/**
* Current in progress action.
*/
readonly pendingAction?: DesignerAction;
/**
* The UI state of the designer, used to restore the state.
*/
designerUIState?: DesignerUIState;
}
export interface DesignerUIState {
activeTabId: PanelTabIdentifier;
}
export type DesignerAction = 'save' | 'initialize' | 'processEdit';
export interface DesignerEditProcessedEventArgs {
result: DesignerEditResult;
edit: DesignerEdit
}
export interface DesignerStateChangedEventArgs {
currentState: DesignerState,
previousState: DesignerState
}
export interface DesignerState {
valid: boolean;
dirty: boolean;
pendingAction?: DesignerAction
}
export const NameProperty = 'name';
export const ScriptProperty = 'script';
export interface DesignerView {
components?: DesignerDataPropertyInfo[]
tabs: DesignerTab[];
}
export interface DesignerTab {
title: string;
components: DesignerDataPropertyInfo[];
}
export interface DesignerViewModel {
[key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties;
}
export interface DesignerDataPropertyInfo {
propertyName: string;
description?: string;
componentType: DesignerComponentTypeName;
group?: string;
componentProperties?: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties;
}
export type DesignerComponentTypeName = 'input' | 'checkbox' | 'dropdown' | 'table';
export interface ComponentProperties {
title?: string;
ariaLabel?: string;
width?: number | string;
enabled?: boolean;
}
export interface CategoryValue {
displayName: string;
name: string;
}
export interface DropDownProperties extends ComponentProperties {
value?: string | CategoryValue;
values?: string[] | CategoryValue[];
}
export interface CheckBoxProperties extends ComponentProperties {
checked?: boolean;
}
export interface InputBoxProperties extends ComponentProperties {
value?: string;
inputType?: 'text' | 'number';
}
export interface DesignerTableProperties extends ComponentProperties {
/**
* the name of the properties to be displayed, properties not in this list will be accessible in details view.
*/
columns?: string[];
/**
* The display name of the object type.
*/
objectTypeDisplayName: string;
/**
* The properties of the table data item.
*/
itemProperties?: DesignerDataPropertyInfo[];
/**
* The data to be displayed.
*/
data?: DesignerTableComponentRowData[];
/**
* Whether user can add new rows to the table. The default value is true.
*/
canAddRows?: boolean;
/**
* Whether user can remove rows from the table. The default value is true.
*/
canRemoveRows?: boolean;
}
export interface DesignerTableComponentRowData {
[key: string]: InputBoxProperties | CheckBoxProperties | DropDownProperties | DesignerTableProperties;
}
export enum DesignerEditType {
Add = 0,
Remove = 1,
Update = 2
}
export interface DesignerEdit {
type: DesignerEditType;
property: DesignerEditIdentifier;
value?: any;
}
export type DesignerEditIdentifier = string | { parentProperty: string, index: number, property: string };
export interface DesignerEditResult {
isValid: boolean;
errors?: { message: string, property?: DesignerEditIdentifier }[];
}
export interface DesignerTextEditor {
/**
* Gets or sets the content of the text editor
*/
content: string;
/**
* Event fired when the content is changed by user
*/
readonly onDidContentChange: Event<string>;
/**
* Update the size of the editor
*/
layout(dimensions: Dimension): void;
}

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.designer-component, .designer-component .container {
width: 100%;
height: 100%;
}
.designer-component .content-container {
display: flex;
flex-direction: column;
border-right-width: 1px;
border-right-style: solid;
width: 100%;
height: 100%;
overflow: hidden;
}
.designer-component .top-content-container {
flex: 0 0 auto;
}
.designer-component .editor-container {
border-top-width: 1px;
border-top-style: solid;
width: 100%;
height: 100%;
}
.designer-component .tabbed-panel-container {
flex: 1 1 auto;
overflow: hidden;
}
.designer-component .properties-container {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.designer-component .properties-container .title-container {
padding: 5px;
flex: 0 0 auto;
font-weight: bold;
}
.designer-component .properties-container .properties-content {
flex: 1 1 auto;
overflow-y: auto;
}
.designer-component .component-label {
vertical-align: middle;
}
.designer-component .components-grid {
display: grid;
/* grid-template-columns: 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;
align-content: start;
box-sizing: border-box;
}
.designer-component .components-grid .full-row {
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;
}
.designer-component .top-content-container .components-grid {
padding-bottom: 10px;
}
.designer-component .content-container .tabbedPanel {
border-width: 0px;
}
.designer-component .components-grid .full-row.group-header {
font-weight: bold;
line-height: 25px;
}