/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/serverGroupDialog'; import { Colorbox } from 'sql/base/browser/ui/colorbox/colorbox'; import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler, attachToggleStyler, attachButtonStyler } from 'vs/platform/theme/common/styler'; import { Event, Emitter } from 'vs/base/common/event'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { localize } from 'vs/nls'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Button } from 'sql/base/browser/ui/button/button'; import { HideReason, Modal } from 'sql/workbench/browser/modal/modal'; import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; import { ServerGroupViewModel } from 'sql/workbench/services/serverGroup/common/serverGroupViewModel'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; import { ILogService } from 'vs/platform/log/common/log'; import { Color } from 'vs/base/common/color'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { attachModalDialogStyler } from 'sql/workbench/common/styler'; import { assertIsDefined, isUndefinedOrNull } from 'vs/base/common/types'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { RequiredIndicatorClassName } from 'sql/base/browser/ui/label/label'; interface IRenderedServerGroupDialog { groupNameInputBox: InputBox; groupDescriptionInputBox: InputBox; serverGroupContainer: HTMLElement; addServerButton: Button; closeButton: Button; } export class ServerGroupDialog extends Modal { private _colorColorBoxesMap: Array<{ color: string, colorbox: Colorbox }> = []; private _selectedColorOption?: number; private _viewModel?: ServerGroupViewModel; private _skipGroupNameValidation: boolean = false; private _onAddServerGroup = new Emitter(); public onAddServerGroup: Event = this._onAddServerGroup.event; private _onCancel = new Emitter(); public onCancel: Event = this._onCancel.event; private _onCloseEvent = new Emitter(); public onCloseEvent: Event = this._onCloseEvent.event; // rendered elements private isRendered = false; private _groupNameInputBox?: InputBox; private _groupDescriptionInputBox?: InputBox; private _serverGroupContainer?: HTMLElement; private _addServerButton?: Button; private _closeButton?: Button; constructor( @ILayoutService layoutService: ILayoutService, @IThemeService themeService: IThemeService, @IContextViewService private _contextViewService: IContextViewService, @IAdsTelemetryService telemetryService: IAdsTelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @IClipboardService clipboardService: IClipboardService, @ILogService logService: ILogService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService ) { super(localize('ServerGroupsDialogTitle', "Server Groups"), TelemetryKeys.ModalDialogName.ServerGroups, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService); } public override render() { super.render(); attachModalDialogStyler(this, this._themeService); const okLabel = localize('serverGroup.ok', "OK"); const cancelLabel = localize('serverGroup.cancel', "Cancel"); this._addServerButton = this.addFooterButton(okLabel, () => this.addGroup()); this._closeButton = this.addFooterButton(cancelLabel, () => this.cancel(), 'right', true); this.isRendered = true; this.registerListeners(); } protected layout(height?: number): void { // NO OP } protected renderBody(container: HTMLElement) { const body = DOM.append(container, DOM.$('.server-group-dialog')); // Connection Group Name const serverGroupNameLabel = localize('connectionGroupName', "Server group name"); DOM.append(body, DOM.$(`.dialog-label.${RequiredIndicatorClassName}`)).innerText = serverGroupNameLabel; this._groupNameInputBox = new InputBox(DOM.append(body, DOM.$('.input-divider')), this._contextViewService, { validationOptions: { validation: (value: string) => !value && !this._skipGroupNameValidation ? ({ type: MessageType.ERROR, content: localize('MissingGroupNameError', "Group name is required.") }) : null }, ariaLabel: serverGroupNameLabel, required: true }); // Connection Group Description const groupDescriptionLabel = localize('groupDescription', "Group description"); DOM.append(body, DOM.$('.dialog-label')).innerText = groupDescriptionLabel; this._groupDescriptionInputBox = new InputBox(DOM.append(body, DOM.$('.input-divider')), this._contextViewService, { ariaLabel: groupDescriptionLabel }); // Connection Group Color const groupColorLabel = localize('groupColor', "Group color"); DOM.append(body, DOM.$('.dialog-label')).innerText = groupColorLabel; this._serverGroupContainer = DOM.append(body, DOM.$('.group-color-options')); this._serverGroupContainer.setAttribute('role', 'radiogroup'); this._serverGroupContainer.setAttribute('aria-label', groupColorLabel); this.fillGroupColors(this._serverGroupContainer); DOM.addStandardDisposableListener(this._serverGroupContainer, DOM.EventType.KEY_DOWN, (event: StandardKeyboardEvent) => { if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.LeftArrow)) { this.preventDefaultKeyboardEvent(event); this.focusNextColor(event.equals(KeyCode.RightArrow)); } }); } private preventDefaultKeyboardEvent(e: StandardKeyboardEvent) { e.preventDefault(); e.stopPropagation(); } private focusNextColor(moveRight: boolean): void { let focusIndex: number = -1; for (let i = 0; i < this._colorColorBoxesMap.length; i++) { if (document.activeElement === this._colorColorBoxesMap[i].colorbox.radioButton) { focusIndex = i; break; } } if (focusIndex >= 0) { if (moveRight) { focusIndex++; } else { focusIndex--; } // check for wraps if (focusIndex < 0) { focusIndex = this._colorColorBoxesMap.length - 1; } else if (focusIndex >= this._colorColorBoxesMap.length) { focusIndex = 0; } this._colorColorBoxesMap[focusIndex].colorbox.focus(); } } private onSelectGroupColor(colorToSelect: string): void { this.withViewModel.groupColor = colorToSelect; this._selectedColorOption = this.withViewModel.colors.indexOf(colorToSelect); this.updateView(); } private registerListeners(): void { const renderedDialog = this.withRenderedDialog; // Theme styler this._register(attachInputBoxStyler(renderedDialog.groupNameInputBox, this._themeService)); this._register(attachInputBoxStyler(renderedDialog.groupDescriptionInputBox, this._themeService)); this._register(attachButtonStyler(renderedDialog.addServerButton, this._themeService)); this._register(attachButtonStyler(renderedDialog.closeButton, this._themeService)); // handler for name change events this._register(renderedDialog.groupNameInputBox.onDidChange(groupName => { this.groupNameChanged(groupName); })); // handler for description change events this._register(renderedDialog.groupDescriptionInputBox.onDidChange(groupDescription => { this.groupDescriptionChanged(groupDescription); })); } private fillGroupColors(container: HTMLElement): void { for (let i = 0; i < this.withViewModel.colors.length; i++) { const color = this.withViewModel.colors[i]; const colorBox = new Colorbox(container, { name: 'server-group-color', label: color }); this._register(colorBox.onSelect((viaKeyboard) => { this.onSelectGroupColor(color); })); colorBox.style({ backgroundColor: Color.fromHex(color) }); // Theme styler this._register(attachToggleStyler(colorBox, this._themeService)); // add the new colorbox to the color map this._colorColorBoxesMap[i] = { color, colorbox: colorBox }; } } private groupNameChanged(groupName: string) { this.withViewModel.groupName = groupName; this.updateView(); } private groupDescriptionChanged(groupDescription: string) { this.withViewModel.groupDescription = groupDescription; this.updateView(); } public get groupName(): string { return this.withRenderedDialog.groupNameInputBox.value; } public get groupDescription(): string { return this.withRenderedDialog.groupDescriptionInputBox.value; } public get selectedColor(): string | undefined { return !isUndefinedOrNull(this._selectedColorOption) ? this._colorColorBoxesMap[this._selectedColorOption].color : undefined; } public get viewModel(): ServerGroupViewModel | undefined { return this._viewModel; } public setViewModel(theViewModel: ServerGroupViewModel) { this._viewModel = theViewModel; if (this._serverGroupContainer) { DOM.clearNode(this._serverGroupContainer); this.fillGroupColors(this._serverGroupContainer); } } public addGroup(): void { if (this.withRenderedDialog.addServerButton.enabled) { if (this.validateInputs()) { this._onAddServerGroup.fire(); } } } public hideError() { this.setError(''); } private validateInputs(): boolean { const renderedDialog = this.withRenderedDialog; const isNameValid = renderedDialog.groupNameInputBox.validate() === undefined; if (!isNameValid) { renderedDialog.groupNameInputBox.focus(); } return isNameValid; } // initialize the view based on the current state of the view model private initializeView(): void { const renderedDialog = this.withRenderedDialog; this.title = this.withViewModel.getDialogTitle(); this._skipGroupNameValidation = true; renderedDialog.groupNameInputBox.value = this.withViewModel.groupName; this._skipGroupNameValidation = false; renderedDialog.groupDescriptionInputBox.value = this.withViewModel.groupDescription ?? ''; this.updateView(); } // update UI elements that have derivative behaviors based on other state changes private updateView(): void { // check the color buttons and if their checked state does not match the view model state then correct it for (let i = 0; i < this._colorColorBoxesMap.length; i++) { let { colorbox: colorbox, color } = this._colorColorBoxesMap[i]; if ((this.withViewModel.groupColor === color) && (colorbox.checked === false)) { colorbox.checked = true; this._selectedColorOption = i; } else if ((this.withViewModel.groupColor !== color) && (colorbox.checked === true)) { colorbox.checked = false; } } // OK button state - enabled if there are pending changes that can be saved this.withRenderedDialog.addServerButton.enabled = this.withViewModel.hasPendingChanges(); } private get withViewModel(): ServerGroupViewModel { return assertIsDefined(this._viewModel); } /* Overwrite escape key behavior */ protected override onClose() { this.cancel(); } /* Overwrite enter key behavior */ protected override onAccept() { this.addGroup(); } public cancel() { this._onCancel.fire(); this.close('cancel'); } public close(hideReason: HideReason = 'close') { this.hide(hideReason); this.withRenderedDialog.groupNameInputBox.hideMessage(); this._onCloseEvent.fire(); } private get withRenderedDialog(): IRenderedServerGroupDialog { if (this.isRendered) { // we assume if we are rendered then all our rendered elements are defined return { groupNameInputBox: this._groupNameInputBox!, groupDescriptionInputBox: this._groupDescriptionInputBox!, serverGroupContainer: this._serverGroupContainer!, addServerButton: this._addServerButton!, closeButton: this._closeButton! }; } else { throw new Error('Server Group Dialog Not Rendered'); } } public open() { // reset the dialog this.hideError(); this.initializeView(); this.show(); this.withRenderedDialog.groupNameInputBox.focus(); } }