/*--------------------------------------------------------------------------------------------- * 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, ITabbedPanelStyles, TabbedPanel } from 'sql/base/browser/ui/panel/panel'; import * as DOM from 'vs/base/browser/dom'; import { Emitter, 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 } 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 { 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 { IThemeService } from 'vs/platform/theme/common/themeService'; import { listActiveSelectionBackground, listActiveSelectionForeground } 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, IDropdownStyles } from 'sql/base/browser/ui/editableDropdown/browser/dropdown'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; export interface IDesignerStyle { tabbedPanelStyles?: ITabbedPanelStyles; inputBoxStyles?: IInputBoxStyles; tableStyles?: ITableStyles; selectBoxStyles?: ISelectBoxStyles; checkboxStyles?: ICheckboxStyles; buttonStyles?: IButtonStyles; dropdownStyles?: IListStyles & IInputBoxStyles & IDropdownStyles; paneSeparator?: Color; groupHeaderBackground?: Color; } export type DesignerUIComponent = InputBox | Checkbox | Table | SelectBox | Dropdown; export type CreateComponentsFunc = (container: HTMLElement, components: DesignerDataPropertyInfo[], parentPath: DesignerPropertyPath) => DesignerUIComponent[]; export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void; interface DesignerTableCellContext { view: DesignerUIArea; path: DesignerPropertyPath; } const ScriptTabId = 'scripts'; const IssuesTabId = 'issues'; 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 _contentTabbedPanel: TabbedPanel; private _scriptTabbedPannel: TabbedPanel; private _contentContainer: HTMLElement; private _topContentContainer: HTMLElement; private _propertiesPaneContainer: HTMLElement; private _styles: IDesignerStyle = {}; private _supressEditProcessing: boolean = false; private _componentMap = new Map(); private _input: DesignerComponentInput; private _tableCellEditorFactory: TableCellEditorFactory; private _propertiesPane: DesignerPropertiesPane; private _buttons: Button[] = []; private _inputDisposable: DisposableStore; private _loadingTimeoutHandle: any; private _groupHeaders: HTMLElement[] = []; private _issuesView: DesignerIssuesTabPanelView; private _scriptEditorView: DesignerScriptEditorTabPanelView; private _onStyleChangeEventEmitter = new Emitter(); 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) { super(); this._tableCellEditorFactory = new TableCellEditorFactory( { valueGetter: (item, column): string => { return item[column.field].value; }, valueSetter: (context: DesignerTableCellContext, row: number, item: DesignerTableComponentRowData, column: Slick.Column, 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; }, editorStyler: (component) => { this.styleComponent(component); }, onStyleChange: this._onStyleChangeEventEmitter.event }, 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 = 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 = 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, this._propertiesPane.groupHeaders, parentPath, 'PropertiesView'); }, (definition, component, viewModel) => { this.setComponentValue(definition, component, viewModel); }); } private styleComponent(component: TabbedPanel | InputBox | Checkbox | Table | SelectBox | Button | Dropdown): 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 if (component instanceof Dropdown) { component.style(this._styles.dropdownStyles); } 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); }); this._propertiesPane.descriptionElement.style.borderColor = styles.paneSeparator.toString(); this._onStyleChangeEventEmitter.fire(); } public layout(dimension: DOM.Dimension) { this._verticalSplitView.layout(dimension.height); this._horizontalSplitView.layout(dimension.width); } public setInput(input: DesignerComponentInput): void { 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(); })); 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 clearUI(): void { this._buttons.forEach(button => button.dispose()); this._buttons = []; this._componentMap.forEach(item => item.component.dispose()); this._componentMap.clear(); DOM.clearNode(this._topContentContainer); this._contentTabbedPanel.clearTabs(); this._propertiesPane.clear(); this._groupHeaders = []; } private initializeDesigner(): void { const view = this._input.view; if (view.components) { this.createComponents(this._topContentContainer, view.components, this._componentMap, this._groupHeaders, DesignerRootObjectPath, 'TopContentView'); } view.tabs.forEach(tab => { this._contentTabbedPanel.pushTab(this.createTabView(tab)); }); this.updateComponentValues(); this.layoutTabbedPanel(); this.updatePropertiesPane(DesignerRootObjectPath); this.restoreUIState(); } 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) { // For tables in the main view, move focus to the first cell of the newly added row, and the properties pane will be showing the new object. if (edit.path.length === 1) { const propertyName = edit.path[0] as string; const tableData = this._input.viewModel[propertyName] as DesignerTableProperties; const table = this._componentMap.get(propertyName).component as Table; try { table.setActiveCell(tableData.data.length - 1, 0); } catch { // Ignore the slick grid error when setting active cell. } } else { this.updatePropertiesPane(this._propertiesPane.objectPath); } } 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(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 = >propertyInfo.component; const targetRow = 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); } } private 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, this._groupHeaders, 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; const tableDataView = table.getData() as TableDataView; 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); } } break; default: break; } this._supressEditProcessing = false; } private createComponents(container: HTMLElement, components: DesignerDataPropertyInfo[], componentMap: Map, groupHeaders: HTMLElement[], 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')); groupHeaders.push(groupHeader); this.styleGroupHeader(groupHeader); 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, 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 = new InputBox(inputContainer, this._contextViewProvider, { ariaLabel: inputProperties.title, type: inputProperties.inputType, }); input.onLoseFocus((args) => { if (args.hasChanged) { this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: args.value, source: view }); } }); 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 = new Dropdown(dropdownContainer, this._contextViewProvider, { values: dropdownProperties.values as string[] || [] }); dropdown.ariaLabel = componentDefinition.componentProperties?.title; dropdown.onValueChange((value) => { this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: value, source: view }); }); dropdown.onFocus(() => { if (view === 'PropertiesView') { this._propertiesPane.updateDescription(componentDefinition); } else if (view === 'TabsView' || view === 'TopContentView') { this.updatePropertiesPane(DesignerRootObjectPath); } }); } else { dropdown = new SelectBox(dropdownProperties.values as string[] || [], undefined, this._contextViewProvider, undefined); dropdown.setAriaLabel(componentDefinition.componentProperties?.title); dropdown.render(dropdownContainer); dropdown.selectElem.style.height = '25px'; dropdown.onDidSelect((e) => { this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: e.selected, source: view }); }); 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 = new Checkbox(checkboxContainer, { label: '', ariaLabel: checkboxProperties.title }); checkbox.onChange((newValue) => { this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: newValue, source: view }); }); 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; if (tableProperties.canAddRows) { const buttonContainer = container.appendChild(DOM.$('.full-row')).appendChild(DOM.$('.add-row-button-container')); const addNewText = tableProperties.labelForAddNewButton ?? localize('designer.newRowText', "Add New"); const addRowButton = new Button(buttonContainer, { title: addNewText, secondary: true }); addRowButton.onDidClick(() => { this.handleEdit({ type: DesignerEditType.Add, path: propertyPath, source: view }); }); this.styleComponent(addRowButton); addRowButton.label = addNewText; addRowButton.icon = { id: `add-row-button new codicon` }; addRowButton.ariaLabel = localize('designer.newRowButtonAriaLabel', "Add new row to '{0}' table", tableProperties.ariaLabel); 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): string => { if (column.field) { return data[column.field]?.value; } else { return undefined; } }, rowHeight: TableRowHeight, headerRowHeight: TableHeaderRowHeight, editorLock: new Slick.EditorLock() }); 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, path: [...propertyPath, e.row, propertyDefinition.propertyName], value: e.value, source: view }); }); return checkboxColumn.definition; case 'dropdown': const dropdownProperties = propertyDefinition.componentProperties as DropDownProperties; return { 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 { 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 deleteRowColumn = new ButtonColumn({ id: 'deleteRow', iconCssClass: Codicon.trash.classNames, title: localize('designer.removeRowText', "Remove"), width: 20, resizable: false, 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); } 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 (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); } } }); 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 saveUIState(): void { if (this._input) { this._input.designerUIState = { activeContentTabId: this._contentTabbedPanel.activeTabId, activeScriptTabId: this._scriptTabbedPannel.activeTabId }; } } private restoreUIState(): void { if (this._input.designerUIState) { this._contentTabbedPanel.showTab(this._input.designerUIState.activeContentTabId); this._scriptTabbedPannel.showTab(this._input.designerUIState.activeScriptTabId); } } }