Files
azuredatastudio/src/sql/workbench/browser/designer/designer.ts
2023-08-07 16:44:23 -07:00

1120 lines
45 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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, DesignerPropertyPath, DesignerViewModel, DesignerDataPropertyInfo,
DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties,
DesignerEditProcessedEventArgs, DesignerStateChangedEventArgs, DesignerAction, ScriptProperty, DesignerRootObjectPath, CanBeDeletedProperty, DesignerUIArea
}
from 'sql/workbench/browser/designer/interfaces';
import { IPanelTab, 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 { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import 'vs/css!./media/designer';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { Table } from 'sql/base/browser/ui/table/table';
import { 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 } from 'sql/workbench/browser/designer/designerPropertiesPane';
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 { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { DesignerIssuesTabPanelView } from 'sql/workbench/browser/designer/designerIssuesTabPanelView';
import { DesignerScriptEditorTabPanelView } from 'sql/workbench/browser/designer/designerScriptEditorTabPanelView';
import { DesignerPropertyPathValidator } from 'sql/workbench/browser/designer/designerPropertyPathValidator';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { asCssVariable, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { layoutDesignerTable, TableHeaderRowHeight, TableRowHeight } from 'sql/workbench/browser/designer/designerTableUtil';
import { Dropdown } from 'sql/base/browser/ui/editableDropdown/browser/dropdown';
import { IAction } from 'vs/base/common/actions';
import { InsertAfterSelectedRowAction, InsertBeforeSelectedRowAction, AddRowAction, DesignerTableActionContext, MoveRowDownAction, MoveRowUpAction, DesignerTableAction } from 'sql/workbench/browser/designer/tableActions';
import { RowMoveManager, RowMoveOnDragEventData } from 'sql/base/browser/ui/table/plugins/rowMoveManager.plugin';
import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
import { listFocusAndSelectionBackground } from 'sql/platform/theme/common/colors';
import { timeout } from 'vs/base/common/async';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IComponentContextService } from 'sql/workbench/services/componentContext/browser/componentContextService';
import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles';
import { ThemeIcon } from 'vs/base/common/themables';
import { defaultCheckboxStyles, defaultEditableDropdownStyles, defaultSelectBoxStyles, getTableStyles } from 'sql/platform/theme/browser/defaultStyles';
import { GroupHeaderBackground } from 'sql/platform/theme/common/colorRegistry';
export type DesignerUIComponent = InputBox | Checkbox | Table<Slick.SlickData> | SelectBox | Dropdown;
export type CreateComponentsFunc = (container: HTMLElement, components: DesignerDataPropertyInfo[], parentPath: DesignerPropertyPath) => DesignerUIComponent[];
export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void;
export interface IDesignerStyles {
paneSeparator: Color;
}
interface DesignerTableCellContext {
view: DesignerUIArea;
path: DesignerPropertyPath;
}
const ScriptTabId = 'scripts';
const IssuesTabId = 'issues';
export class Designer extends Disposable {
private _loadingSpinner: LoadingSpinner;
private _horizontalSplitViewContainer: HTMLElement;
private _verticalSplitViewContainer: HTMLElement;
private _tabbedPanelContainer: HTMLElement;
private _editorContainer: HTMLElement;
private _horizontalSplitView: SplitView;
private _verticalSplitView: SplitView;
private _contentTabbedPanel: TabbedPanel;
private _scriptTabbedPannel: TabbedPanel;
private _contentContainer: HTMLElement;
private _topContentContainer: HTMLElement;
private _propertiesPaneContainer: HTMLElement;
private _supressEditProcessing: boolean = false;
private _componentMap = new Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>();
private _input: DesignerComponentInput;
private _tableCellEditorFactory: TableCellEditorFactory;
private _propertiesPane: DesignerPropertiesPane;
private _inputDisposable: DisposableStore;
private _loadingTimeoutHandle: any;
private _issuesView: DesignerIssuesTabPanelView;
private _scriptEditorView: DesignerScriptEditorTabPanelView;
private _taskbars: Taskbar[] = [];
private _actionsMap: Map<Taskbar, DesignerTableAction[]> = new Map<Taskbar, DesignerTableAction[]>();
constructor(private readonly _container: HTMLElement,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IContextViewService private readonly _contextViewProvider: IContextViewService,
@INotificationService private readonly _notificationService: INotificationService,
@IDialogService private readonly _dialogService: IDialogService,
@IThemeService private readonly _themeService: IThemeService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IComponentContextService private readonly _componentContextService: IComponentContextService) {
super();
this._tableCellEditorFactory = new TableCellEditorFactory(
{
valueGetter: (item, column): string => {
return item[column.field].value;
},
valueSetter: (context: DesignerTableCellContext, row: number, item: DesignerTableComponentRowData, column: Slick.Column<Slick.SlickData>, value: string): void => {
this.handleEdit({
type: DesignerEditType.Update,
path: [...context.path, row, column.field],
value: value,
source: context.view
});
},
optionsGetter: (item, column): string[] => {
return item[column.field].values;
},
inputBoxStyles: defaultInputBoxStyles,
editableDropdownStyles: defaultEditableDropdownStyles,
selectBoxStyles: defaultSelectBoxStyles
}, 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._contentTabbedPanel = this._register(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._scriptTabbedPannel = this._register(new TabbedPanel(this._editorContainer));
this._issuesView = this._instantiationService.createInstance(DesignerIssuesTabPanelView);
this._register(this._issuesView.onIssueSelected((path) => {
if (path && path.length > 0) {
this.selectProperty(path);
}
}));
this._scriptEditorView = new DesignerScriptEditorTabPanelView(this._instantiationService);
this._scriptTabbedPannel.pushTab({
title: localize('designer.scriptTabTitle', "Scripts"),
identifier: ScriptTabId,
view: this._scriptEditorView
});
this._verticalSplitView.addView({
element: this._editorContainer,
layout: size => {
this._scriptTabbedPannel.layout(new DOM.Dimension(this._editorContainer.clientWidth, size - this._scriptTabbedPannel.headersize));
},
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 => {
this.layoutPropertiesPane();
},
minimumSize: 200,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
}, Sizing.Distribute);
this._propertiesPane = new DesignerPropertiesPane(this._propertiesPaneContainer, (container, components, parentPath) => {
return this.createComponents(container, components, this._propertiesPane.componentMap, parentPath, 'PropertiesView');
}, (definition, component, viewModel) => {
this.setComponentValue(definition, component, viewModel);
}, this._instantiationService);
}
public style(styles: IDesignerStyles): void {
this._verticalSplitView.style({
separatorBorder: styles.paneSeparator
});
this._horizontalSplitView.style({
separatorBorder: styles.paneSeparator
});
this._propertiesPane.descriptionElement.style.borderColor = styles.paneSeparator.toString();
}
public layout(dimension: DOM.Dimension) {
this._verticalSplitView.layout(dimension.height);
this._horizontalSplitView.layout(dimension.width);
}
public setInput(input: DesignerComponentInput): void {
if (this._input) {
void this.submitPendingChanges().catch(onUnexpectedError);
}
this.saveUIState();
if (this._loadingTimeoutHandle) {
this.stopLoading();
}
this.clearUI();
this._inputDisposable?.dispose();
// 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);
}));
this._inputDisposable.add(this._input.onRefreshRequested(() => {
this.refresh();
}));
this._inputDisposable.add(this._input.onSubmitPendingEditRequested(async () => {
await this.submitPendingChanges();
}));
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();
}
public async submitPendingChanges(): Promise<void> {
if (this._container.contains(document.activeElement) && document.activeElement instanceof HTMLInputElement) {
// Force the elements to fire the blur event to submit the pending changes.
document.activeElement.blur();
return timeout(10);
}
}
private clearUI(): void {
this._componentMap.forEach(item => item.component.dispose());
this._componentMap.clear();
DOM.clearNode(this._topContentContainer);
this._contentTabbedPanel.clearTabs();
this._propertiesPane.clear();
this._taskbars.map(t => t.dispose());
}
private initializeDesigner(): void {
const view = this._input.view;
if (view.components) {
this.createComponents(this._topContentContainer, view.components, this._componentMap, DesignerRootObjectPath, 'TopContentView');
}
view.tabs.forEach(tab => {
this._contentTabbedPanel.pushTab(this.createTabView(tab));
});
this.updateComponentValues();
this.layoutTabbedPanel();
this.updatePropertiesPane(DesignerRootObjectPath);
this.restoreUIState();
}
private handleCellFocusAfterAddOrMove(edit: DesignerEdit): void {
if (edit.path.length === 2) {
const propertyName = edit.path[0] as string;
const index = edit.type === DesignerEditType.Add ? edit.path[1] as number : edit.value as number;
const table = this._componentMap.get(propertyName).component as Table<Slick.SlickData>;
const tableProperties = this._componentMap.get(propertyName).defintion.componentProperties as DesignerTableProperties;
let selectedCellIndex = tableProperties.itemProperties.findIndex(p => p.componentType === 'input');
selectedCellIndex = tableProperties.canMoveRows ? selectedCellIndex + 1 : selectedCellIndex;
try {
table.grid.resetActiveCell();
table.setActiveCell(index, selectedCellIndex);
table.setSelectedRows([index]);
}
catch {
// Ignore the slick grid error when setting active cell.
}
} else {
this.updatePropertiesPane(this._propertiesPane.objectPath);
}
}
private handleEditProcessedEvent(args: DesignerEditProcessedEventArgs): void {
const edit = args.edit;
this._supressEditProcessing = true;
if (args.result.issues?.length > 0) {
alert(localize('designer.issueCountAlert', "{0} validation issues found.", args.result.issues.length));
}
try {
if (args.result.refreshView) {
this.refresh();
if (!args.result.isValid) {
this._scriptTabbedPannel.showTab(IssuesTabId);
}
} else {
this.updateComponentValues();
this.layoutTabbedPanel();
}
if (edit.type === DesignerEditType.Add || edit.type === DesignerEditType.Move) {
this.handleCellFocusAfterAddOrMove(edit);
} else if (edit.type === DesignerEditType.Update) {
// for edit, update the properties pane with new values of current object.
this.updatePropertiesPane(this._propertiesPane.objectPath);
} else if (edit.type === DesignerEditType.Remove) {
// removing the secondary level entities, the properties pane needs to be updated to reflect the changes.
if (edit.path.length === 4) {
this.updatePropertiesPane(this._propertiesPane.objectPath);
}
}
// try to move the focus back to where it was
if (args.result.refreshView) {
this.selectProperty(args.edit.path, args.edit.source, false);
}
} catch (err) {
this._notificationService.error(err);
}
this._supressEditProcessing = false;
}
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 'publish':
message = showLoading ? localize('designer.publishingChanges', "Publishing changes...") : localize('designer.publishChangesCompleted', "Changes have been published");
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:
message = showLoading ? localize('designer.processing', "Processing...") : localize('designer.processingCompleted', "Processing completed");
timeout = 0;
break;
}
if (showLoading) {
this.startLoading(message, useDelay ? timeout : 0);
} else {
this.stopLoading(message);
}
}
private refresh() {
this.saveUIState();
this.clearUI();
this.initializeDesigner();
}
private layoutTabbedPanel() {
this._contentTabbedPanel.layout(new DOM.Dimension(this._tabbedPanelContainer.clientWidth, this._tabbedPanelContainer.clientHeight));
}
private layoutPropertiesPane() {
this._propertiesPane?.componentMap.forEach((v) => {
if (v.component instanceof Table) {
layoutDesignerTable(v.component, this._propertiesPaneContainer.clientWidth);
}
});
}
private updatePropertiesPane(objectPath: DesignerPropertyPath): void {
let type: string;
let components: DesignerDataPropertyInfo[];
let objectViewModel: DesignerViewModel;
if (objectPath.length === 0) { // root object
type = this._input.objectTypeDisplayName;
components = [];
components.push(...this._input.view.components);
this._input.view.tabs.forEach(tab => {
components.push(...tab.components);
});
objectViewModel = this._input.viewModel;
} else if (objectPath.length === 2) { // second level object
const parentPropertyName = objectPath[0] as string;
const objectIndex = objectPath[1] as number;
const tableData = this._input.viewModel[parentPropertyName] as DesignerTableProperties;
const tableProperties = this._componentMap.get(parentPropertyName).defintion.componentProperties as DesignerTableProperties;
objectViewModel = tableData.data[objectIndex] as DesignerViewModel;
components = tableProperties.itemProperties;
type = tableProperties.objectTypeDisplayName;
}
this._propertiesPane.show({
path: objectPath,
type: type,
components: components,
viewModel: objectViewModel
});
this.layoutPropertiesPane();
}
private updateComponentValues(): void {
this.updateIssuesTab();
const viewModel = this._input.viewModel;
const scriptProperty = viewModel[ScriptProperty] as InputBoxProperties;
if (scriptProperty) {
this._scriptEditorView.content = scriptProperty.value || '';
}
this._componentMap.forEach((value) => {
this.setComponentValue(value.defintion, value.component, viewModel);
});
}
private updateIssuesTab(): void {
if (!this._input) {
return;
}
if (this._scriptTabbedPannel.contains(IssuesTabId)) {
this._scriptTabbedPannel.removeTab(IssuesTabId);
}
if (this._input.issues === undefined || this._input.issues.length === 0) {
return;
}
this._scriptTabbedPannel.pushTab({
title: localize('designer.issuesTabTitle', "Issues ({0})", this._input.issues.length),
identifier: IssuesTabId,
view: this._issuesView
});
this._scriptTabbedPannel.showTab(IssuesTabId);
this._issuesView.updateIssues(this._input.issues);
}
private selectProperty(path: DesignerPropertyPath, view?: DesignerUIArea, highlight: boolean = true): void {
if (!DesignerPropertyPathValidator.validate(path, this._input.viewModel)) {
return;
}
// Find top level property
let found = false;
if (this._input.view.components) {
for (const component of this._input.view.components) {
if (path[0] === component.propertyName) {
found = true;
break;
}
}
}
if (this._input.view.tabs) {
for (const tab of this._input.view.tabs) {
if (tab) {
for (const component of tab.components) {
if (path[0] === component.propertyName) {
// if we are editing the top level property and the view is properties view, then we don't have to switch to the tab.
if (path.length !== 1 || view !== 'PropertiesView') {
this._contentTabbedPanel.showTab(tab.title);
}
found = true;
break;
}
}
}
if (found) {
break;
}
}
}
if (found) {
const propertyInfo = this._componentMap.get(<string>path[0]);
if (propertyInfo.defintion.componentType !== 'table') {
if (view === 'PropertiesView') {
this.updatePropertiesPane(DesignerRootObjectPath);
this._propertiesPane.selectProperty(path);
} else {
propertyInfo.component.focus();
}
return;
} else {
const tableComponent = <Table<Slick.SlickData>>propertyInfo.component;
const targetRow = <number>path[1];
const targetCell = 0;
tableComponent.setActiveCell(targetRow, targetCell);
tableComponent.grid.scrollCellIntoView(targetRow, targetCell, false);
if (path.length > 2) {
const relativePath = path.slice(2);
this._propertiesPane.selectProperty(relativePath);
}
}
if (highlight) {
this.highlightActiveElement();
}
}
}
private highlightActiveElement(): void {
const bgColor = this._themeService.getColorTheme().getColor(listActiveSelectionBackground);
const color = this._themeService.getColorTheme().getColor(listActiveSelectionForeground);
const currentElement = document.activeElement as HTMLElement;
if (currentElement) {
const originalBGColor = currentElement.style.backgroundColor;
const originalColor = currentElement.style.color;
currentElement.style.backgroundColor = bgColor.toString();
currentElement.style.color = color.toString();
setTimeout(() => {
currentElement.style.color = originalColor;
currentElement.style.backgroundColor = originalBGColor;
}, 500);
}
}
public handleEdit(edit: DesignerEdit): void {
if (this._supressEditProcessing) {
return;
}
this._input.processEdit(edit);
}
private createTabView(tab: DesignerTab): IPanelTab {
const view = new DesignerTabPanelView(tab, (container, components, identifierGetter) => {
return this.createComponents(container, components, this._componentMap, identifierGetter, 'TabsView');
});
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 dropdownProperties = definition.componentProperties as DropDownProperties;
const dropdownData = viewModel[definition.propertyName] as DropDownProperties;
const options = (dropdownData.values || dropdownProperties.values || []) as string[];
const idx = options?.indexOf(dropdownData.value as string);
let dropdown: Dropdown | SelectBox;
if (dropdownProperties.isEditable) {
dropdown = component as Dropdown;
if (dropdownData.enabled === false) {
dropdown.enabled = false;
} else {
dropdown.enabled = true;
}
dropdown.values = options;
if (idx > -1) {
dropdown.value = options[idx];
}
} else {
dropdown = component as SelectBox;
if (dropdownData.enabled === false) {
dropdown.disable();
} else {
dropdown.enable();
}
dropdown.setOptions(options);
if (idx > -1) {
dropdown.select(idx);
}
}
this._register(dropdown);
break;
default:
break;
}
this._supressEditProcessing = false;
}
private createComponents(container: HTMLElement,
components: DesignerDataPropertyInfo[],
componentMap: Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>,
parentPath: DesignerPropertyPath,
area: DesignerUIArea): DesignerUIComponent[] {
const uiComponents = [];
const groupNames = [];
const componentsToCreate = area === 'PropertiesView' ? components.filter(component => component.showInPropertiesView !== false) : components;
componentsToCreate.forEach(component => {
// Set the default group name if not set (undefined or null).
component.group = component.group || localize('designer.generalGroupName', "General");
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, parentPath, componentMap, area));
});
} else {
groupNames.forEach(group => {
const groupHeader = container.appendChild(DOM.$('div.full-row.group-header'));
groupHeader.style.backgroundColor = asCssVariable(GroupHeaderBackground);
groupHeader.innerText = group;
componentsToCreate.forEach(component => {
if (component.group === group) {
uiComponents.push(this.createComponent(container, component, parentPath, componentMap, area));
}
});
});
}
return uiComponents;
}
private createComponent(container: HTMLElement,
componentDefinition: DesignerDataPropertyInfo,
parentPath: DesignerPropertyPath,
componentMap: Map<string, { defintion: DesignerDataPropertyInfo, component: DesignerUIComponent }>,
view: DesignerUIArea): DesignerUIComponent {
const propertyPath = [...parentPath, componentDefinition.propertyName];
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 = this._register(new InputBox(inputContainer, this._contextViewProvider, {
ariaLabel: inputProperties.title,
type: inputProperties.inputType,
ariaDescription: componentDefinition.description,
inputBoxStyles: defaultInputBoxStyles
}));
this._register(input.onLoseFocus((args) => {
if (args.hasChanged) {
this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: args.value, source: view });
}
}));
this._register(input.onInputFocus(() => {
if (view === 'PropertiesView') {
this._propertiesPane.updateDescription(componentDefinition);
} else if (view === 'TabsView' || view === 'TopContentView') {
this.updatePropertiesPane(DesignerRootObjectPath);
}
}));
if (view === 'TopContentView' && inputProperties.width) {
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;
let dropdown;
if (dropdownProperties.isEditable) {
dropdown = this._register(new Dropdown(dropdownContainer, this._contextViewProvider, {
values: dropdownProperties.values as string[] || [],
ariaLabel: componentDefinition.componentProperties?.title,
ariaDescription: componentDefinition.description,
...defaultEditableDropdownStyles
}));
this._register(dropdown.onValueChange((value) => {
this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: value, source: view });
}));
this._register(dropdown.onFocus(() => {
if (view === 'PropertiesView') {
this._propertiesPane.updateDescription(componentDefinition);
} else if (view === 'TabsView' || view === 'TopContentView') {
this.updatePropertiesPane(DesignerRootObjectPath);
}
}));
} else {
dropdown = this._register(new SelectBox(dropdownProperties.values as string[] || [], undefined, defaultSelectBoxStyles, this._contextViewProvider, undefined, {
ariaLabel: componentDefinition.componentProperties?.title,
ariaDescription: componentDefinition.description
}));
dropdown.render(dropdownContainer);
dropdown.selectElem.style.height = '25px';
this._register(dropdown.onDidSelect((e) => {
this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: e.selected, source: view });
}));
this._register(dropdown.onDidFocus(() => {
if (view === 'PropertiesView') {
this._propertiesPane.updateDescription(componentDefinition);
} else if (view === 'TabsView' || view === 'TopContentView') {
this.updatePropertiesPane(DesignerRootObjectPath);
}
}));
}
component = dropdown;
break;
case 'checkbox':
container.appendChild(DOM.$('')).appendChild(DOM.$('span.component-label')).innerText = componentDefinition.componentProperties?.title ?? '';
const checkboxContainer = container.appendChild(DOM.$(''));
const checkboxProperties = componentDefinition.componentProperties as CheckBoxProperties;
const checkbox = this._register(new Checkbox(checkboxContainer, { ...defaultCheckboxStyles, label: '', ariaLabel: checkboxProperties.title, ariaDescription: componentDefinition.description }));
this._register(checkbox.onChange((newValue) => {
this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: newValue, source: view });
}));
this._register(checkbox.onFocus(() => {
if (view === 'PropertiesView') {
this._propertiesPane.updateDescription(componentDefinition);
} else if (view === 'TabsView' || view === 'TopContentView') {
this.updatePropertiesPane(DesignerRootObjectPath);
}
}));
component = checkbox;
break;
case 'table':
if (view === 'PropertiesView') {
container.appendChild(DOM.$('.full-row')).appendChild(DOM.$('span.component-label')).innerText = componentDefinition.componentProperties?.title ?? '';
}
const tableProperties = componentDefinition.componentProperties as DesignerTableProperties;
const taskbar = this.addTableTaskbar(container, tableProperties);
const tableContainer = container.appendChild(DOM.$('.full-row'));
const table = this._register(new Table(tableContainer, this._accessibilityService, this._quickInputService, getTableStyles({
listActiveSelectionBackground: undefined,
listActiveSelectionForeground: undefined,
listFocusAndSelectionBackground: undefined,
listFocusAndSelectionForeground: undefined,
listInactiveFocusBackground: undefined,
listInactiveSelectionBackground: undefined,
listInactiveSelectionForeground: undefined
}), {
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;
}
},
rowHeight: TableRowHeight,
headerRowHeight: TableHeaderRowHeight,
editorLock: new Slick.EditorLock()
}));
table.grid.setSelectionModel(new RowSelectionModel());
if (taskbar) {
taskbar.context = { table: table, path: propertyPath, source: view };
this._actionsMap.get(taskbar).map(a => a.table = table);
}
const columns: Slick.Column<Slick.SlickData>[] = [];
if (tableProperties.canMoveRows) {
// Add row move drag and drop
const moveRowsPlugin = new RowMoveManager({
cancelEditOnDrag: true,
id: 'moveRow',
iconCssClass: ThemeIcon.asClassName(Codicon.grabber),
name: localize('designer.moveRowText', 'Move'),
width: 50,
resizable: true,
isFontIcon: true,
behavior: 'selectAndMove'
});
table.registerPlugin(moveRowsPlugin);
moveRowsPlugin.onMoveRows.subscribe((e: Slick.EventData, data: RowMoveOnDragEventData) => {
const row = data.rows[0];
// no point in moving before or after itself
if (row === data.insertBefore || row === data.insertBefore - 1) {
e.stopPropagation();
return;
}
this.handleEdit({
type: DesignerEditType.Move,
path: [...propertyPath, row],
source: view,
value: data.insertBefore < row ? data.insertBefore : data.insertBefore - 1
});
});
table.grid.registerPlugin(moveRowsPlugin);
columns.push(moveRowsPlugin.definition);
}
table.ariaLabel = tableProperties.ariaLabel;
columns.push(...tableProperties.columns.map((propName, index) => {
const propertyDefinition = tableProperties.itemProperties.find(item => item.propertyName === propName);
switch (propertyDefinition.componentType) {
case 'checkbox':
const checkboxColumn = new CheckBoxColumn({
id: index.toString(),
field: propertyDefinition.propertyName,
name: propertyDefinition.componentProperties.title,
width: propertyDefinition.componentProperties.width as number
});
table.registerPlugin(checkboxColumn);
this._register(checkboxColumn.onChange((e) => {
this.handleEdit({
type: DesignerEditType.Update,
path: [...propertyPath, e.row, propertyDefinition.propertyName],
value: e.value,
source: view
});
}));
return checkboxColumn.definition;
case 'dropdown':
const dropdownProperties = propertyDefinition.componentProperties as DropDownProperties;
return {
id: index.toString(),
name: dropdownProperties.title,
field: propertyDefinition.propertyName,
editor: this._tableCellEditorFactory.getDropdownEditorClass({ view: view, path: propertyPath }, dropdownProperties.values as string[], dropdownProperties.isEditable),
width: dropdownProperties.width as number
};
default:
const inputProperties = propertyDefinition.componentProperties as InputBoxProperties;
return {
id: index.toString(),
name: inputProperties.title,
field: propertyDefinition.propertyName,
editor: this._tableCellEditorFactory.getTextEditorClass({ view: view, path: propertyPath }, inputProperties.inputType),
width: inputProperties.width as number
};
}
}));
if (tableProperties.canRemoveRows) {
const removeText = localize('designer.removeRowText', "Remove");
const deleteRowColumn = new ButtonColumn({
id: 'deleteRow',
iconCssClass: ThemeIcon.asClassName(Codicon.trash),
name: removeText,
title: removeText,
width: 60,
resizable: true,
isFontIcon: true,
enabledField: CanBeDeletedProperty
});
deleteRowColumn.onClick(async (e) => {
if (tableProperties.showRemoveRowConfirmation) {
const confirmMessage = tableProperties.removeRowConfirmationMessage || localize('designer.defaultRemoveRowConfirmationMessage', "Are you sure you want to remove the row?");
const result = await this._dialogService.confirm({
type: 'question',
message: confirmMessage
});
if (!result.confirmed) {
return;
}
}
this.handleEdit({
type: DesignerEditType.Remove,
path: [...propertyPath, e.row],
source: view
});
});
table.registerPlugin(deleteRowColumn);
columns.push(deleteRowColumn.definition);
}
if (tableProperties.canInsertRows || tableProperties.canMoveRows) {
const moreActionsText = localize('designer.actions', "More Actions");
const actionsColumn = new ButtonColumn({
id: 'actions',
iconCssClass: ThemeIcon.asClassName(Codicon.ellipsis),
name: moreActionsText,
title: moreActionsText,
width: 100,
resizable: true,
isFontIcon: true
});
this._register(actionsColumn.onClick((e) => {
this.openContextMenu(table, e.row, e.position, propertyPath, view, tableProperties);
}));
table.registerPlugin(actionsColumn);
columns.push(actionsColumn.definition);
// Add move context menu actions
this._register(table.onContextMenu((e) => {
this.openContextMenu(table, e.cell.row, e.anchor, propertyPath, view, tableProperties);
}));
}
table.columns = columns;
table.grid.onBeforeEditCell.subscribe((e, data): boolean => {
return data.item[data.column.field].enabled !== false;
});
let currentTableActions = [];
if (taskbar) {
currentTableActions = this._actionsMap.get(taskbar);
}
table.grid.onActiveCellChanged.subscribe((e, data) => {
if (view === 'TabsView' || view === 'TopContentView') {
if (data.row !== undefined) {
if (tableProperties.showItemDetailInPropertiesView === false) {
this.updatePropertiesPane(DesignerRootObjectPath);
} else {
this.updatePropertiesPane([...propertyPath, data.row]);
}
} else {
this.updatePropertiesPane(DesignerRootObjectPath);
}
} else if (view === 'PropertiesView') {
if (data.row !== undefined) {
this._propertiesPane.updateDescription(componentDefinition);
}
}
if (data.row !== undefined) {
currentTableActions.forEach(a => a.updateState(data.row));
table.grid.setSelectedRows([data.row]);
}
});
table.onBlur((e) => {
currentTableActions.forEach(a => a.updateState());
table.grid.setSelectedRows([]);
});
this._register(this._componentContextService.registerTable(table));
component = table;
break;
default:
throw new Error(localize('designer.unknownComponentType', "The component type: {0} is not supported", componentDefinition.componentType));
}
componentMap.set(componentDefinition.propertyName, {
defintion: componentDefinition,
component: component
});
return component;
}
private addTableTaskbar(container: HTMLElement, tableProperties: DesignerTableProperties): Taskbar | undefined {
if (tableProperties.canAddRows || tableProperties.canMoveRows) {
const taskbarContainer = container.appendChild(DOM.$('.full-row')).appendChild(DOM.$('.add-row-button-container'));
const taskbar = new Taskbar(taskbarContainer);
const actions = [];
if (tableProperties.canAddRows) {
const addRowAction = this._instantiationService.createInstance(AddRowAction, this, tableProperties);
actions.push(addRowAction);
}
if (tableProperties.canMoveRows) {
const moveUpAction = this._instantiationService.createInstance(MoveRowUpAction, this);
const moveDownAction = this._instantiationService.createInstance(MoveRowDownAction, this);
actions.push(moveUpAction);
actions.push(moveDownAction);
}
const taskbarContent: ITaskbarContent[] = actions.map((a) => { return { action: a }; });
taskbar.setContent(taskbarContent);
this._actionsMap.set(taskbar, actions);
return taskbar;
}
return undefined;
}
private openContextMenu(
table: Table<Slick.SlickData>,
rowIndex: number,
anchor: HTMLElement | { x: number, y: number },
propertyPath: DesignerPropertyPath,
view: DesignerUIArea,
tableProperties: DesignerTableProperties
): void {
const tableActionContext: DesignerTableActionContext = {
table: table,
path: propertyPath,
source: view,
selectedRow: rowIndex
};
const data = table.grid.getData() as Slick.DataProvider<Slick.SlickData>;
if (!data || rowIndex >= data.getLength()) {
return undefined;
}
const actions = this.getTableActions(tableProperties);
actions.forEach(a => {
if (a instanceof DesignerTableAction) {
a.table = table;
a.updateState(rowIndex);
}
});
this._contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => actions,
getActionsContext: () => (tableActionContext)
});
}
private getTableActions(tableProperties: DesignerTableProperties): IAction[] {
const actions: IAction[] = [];
if (tableProperties.canInsertRows) {
const insertRowBefore = this._instantiationService.createInstance(InsertBeforeSelectedRowAction, this);
const insertRowAfter = this._instantiationService.createInstance(InsertAfterSelectedRowAction, this);
actions.push(insertRowBefore);
actions.push(insertRowAfter);
}
if (tableProperties.canMoveRows) {
const moveRowUp = this._instantiationService.createInstance(MoveRowUpAction, this);
const moveRowDown = this._instantiationService.createInstance(MoveRowDownAction, this);
actions.push(moveRowUp);
actions.push(moveRowDown);
}
return actions;
}
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 saveUIState(): void {
if (this._input) {
this._input.designerUIState = {
activeContentTabId: this._contentTabbedPanel.selectedTabId,
activeScriptTabId: this._scriptTabbedPannel.selectedTabId
};
}
}
private restoreUIState(): void {
if (this._input.designerUIState) {
this._contentTabbedPanel.showTab(this._input.designerUIState.activeContentTabId);
this._scriptTabbedPannel.showTab(this._input.designerUIState.activeScriptTabId);
}
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const listHoverBackgroundColor = theme.getColor(listHoverBackground);
const listActiveSelectionBackgroundColor = theme.getColor(listActiveSelectionBackground);
const listFocusSelectionBackgroundColor = theme.getColor(listFocusAndSelectionBackground);
if (listHoverBackgroundColor) {
collector.addRule(`
.designer-component .slick-cell.isDragging {
background-color: ${listHoverBackgroundColor};
}
.designer-component .slick-reorder-proxy {
background: ${listActiveSelectionBackgroundColor};
opacity: 0.5;
}
.vs-dark .designer-component .slick-reorder-proxy {
opacity: 0.3;
}
.designer-component .slick-reorder-guide {
background: ${listFocusSelectionBackgroundColor};
opacity: 1;
}
`);
}
});