SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Builder } from 'vs/base/browser/builder';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { Button } from 'vs/base/browser/ui/button/button';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import * as data from 'data';
import * as types from 'vs/base/common/types';
export function appendRow(container: Builder, label: string, labelClass: string, cellContainerClass: string): Builder {
let cellContainer: Builder;
container.element('tr', {}, (rowContainer) => {
rowContainer.element('td', { class: labelClass }, (labelCellContainer) => {
labelCellContainer.div({}, (labelContainer) => {
labelContainer.innerHtml(label);
});
});
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
cellContainer = inputCellContainer;
});
});
return cellContainer;
}
export function appendRowLink(container: Builder, label: string, labelClass: string, cellContainerClass: string): Builder {
let rowButton: Button;
container.element('tr', {}, (rowContainer) => {
rowContainer.element('td', { class: labelClass }, (labelCellContainer) => {
labelCellContainer.div({}, (labelContainer) => {
labelContainer.innerHtml(label);
});
});
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
inputCellContainer.element('div', {}, (rowContainer) => {
rowButton = new Button(rowContainer);
});
});
});
return new Builder(rowButton.getElement());
}
export function createCheckBox(container: Builder, label: string, checkboxClass: string, isChecked: boolean, onCheck?: (viaKeyboard: boolean) => void): Checkbox {
let checkbox = new Checkbox({
actionClassName: checkboxClass,
title: label,
isChecked: isChecked,
onChange: (viaKeyboard) => {
if (onCheck) {
onCheck(viaKeyboard);
}
}
});
container.getHTMLElement().appendChild(checkbox.domNode);
container.div({}, (labelContainer) => {
labelContainer.innerHtml(label);
});
return checkbox;
}
export function appendInputSelectBox(container: Builder, selectBox: SelectBox): SelectBox {
selectBox.render(container.getHTMLElement());
return selectBox;
}
export function isNullOrWhiteSpace(value: string): boolean {
// returns true if the string is null or contains white space/tab chars only
return !value || value.trim().length === 0;
}
export function getBooleanValueFromStringOrBoolean(value: any): boolean {
if (types.isBoolean(value)) {
return value;
} else if (types.isString(value)) {
return value.toLowerCase() === 'true';
}
return false;
}
export function getCategoryDisplayName(categories: data.CategoryValue[], categoryName: string) {
var displayName: string;
categories.forEach(c => {
if (c.name === categoryName) {
displayName = c.displayName;
}
});
return displayName;
}
export function getCategoryName(categories: data.CategoryValue[], categoryDisplayName: string) {
var categoryName: string;
categories.forEach(c => {
if (c.displayName === categoryDisplayName) {
categoryName = c.name;
}
});
return categoryName;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}</style></defs><title>back_16x16</title><path class="cls-1" d="M16.15,8.5H2.1l6.15,6.15-.7.7L.19,8,7.55.65l.7.7L2.1,7.5h14Z"/></svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>back_inverse_16x16</title><path class="cls-1" d="M16.15,8.5H2.1l6.15,6.15-.7.7L.19,8,7.55.65l.7.7L2.1,7.5h14Z"/></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -0,0 +1,178 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-shell .modal {
background-color: rgba(204, 204, 204, 0.6);
right: 0;
left: 0;
top: 0;
bottom: 0;
position: absolute;
z-index: 500;
}
.monaco-shell .modal:not(.flyout-dialog) .modal-dialog {
margin: auto;
width: 640px;
height: 480px;
}
.modal .modal-header {
padding: 15px;
}
.modal .modal-footer {
padding: 15px;
}
.modal .icon.in-progress {
width: 25px;
height: 25px;
}
.monaco-shell .modal.flyout-dialog .modal-dialog {
margin: auto auto auto auto;
height: 100%;
width: 500px;
right: 0;
position: absolute;
overflow-y: hidden;
}
.monaco-shell .modal.flyout-dialog.wide .modal-dialog {
width: 1200px;
max-width: 95%;
min-width: 400px;
}
.monaco-shell .modal.flyout-dialog .modal-content {
height: 100%;
font-size: 11px;
}
.modal .modal-title {
font-size: 15px;
}
.modal .modal-title-icon {
width: 20px;
height: 20px;
float: left;
margin-right: 10px;
}
.monaco-shell .modal.flyout-dialog .modal-body {
height: calc(100% - 105px);
}
/* modal body for angular component dialog */
.monaco-shell .modal.flyout-dialog .angular-modal-body {
height: calc(100% - 90px);
}
/* Style for body and footer in modal dialog. This should be applied to dialog created with angular component. */
.monaco-shell .modal.flyout-dialog .modal-body-and-footer {
height: 100%;
}
/* modl body content style(excluding dialogErrorMessage section) for angulr component dialog */
.angular-modal-body-content {
overflow-x: hidden;
overflow-y: auto;
padding: 15px;
}
.modal.flyout-dialog .angular-form {
height: 100%;
}
.modal.flyout-dialog .dialog-label {
width: 100%;
padding-bottom: 5px;
}
/* Add space in the bottom to separate each input elements */
.modal.flyout-dialog .input-divider {
width: 100%;
padding-bottom: 20px;
}
.modal.flyout-dialog .input {
width: 100%;
border: none;
height: 25px;
padding-left: 4px;
}
.modal.flyout-dialog .modal-body {
overflow-y: auto;
}
.vs-dark.monaco-shell .modal.flyout-dialog .input {
background-color: #3C3C3C;
}
.vs-dark.monaco-shell .modal.flyout-dialog input:disabled {
background-color: #E1E1E1;
color: #3C3C3C;
}
.modal .select-box {
width: 100%;
height: 25px;
color: #6C6C6C;
font-size: 11px;
border: 1px solid transparent;
}
.modal.flyout-dialog .dialogErrorMessage {
overflow: hidden;
padding-left: 10px;
overflow-y: auto;
display: flex;
}
.modal .modal-footer {
display: flex;
}
.modal .modal-footer .left-footer {
display: flex;
flex: 1 1 auto;
justify-content: flex-start;
}
.modal .modal-footer .right-footer {
display: flex;
flex: 1 1 auto;
justify-content: flex-end;
}
.modal .footer-button a.monaco-button.monaco-text-button {
width: 100px;
}
.vs-dark.monaco-shell .modal.flyout-dialog .footer-button a.monaco-button.monaco-text-button {
outline-color: #8e8c8c;
}
.modal.flyout-dialog .footer-button a.monaco-button.monaco-text-button:focus {
outline-width: 1px;
}
.modal .footer-button {
margin-right: 5px;
}
.modal .right-footer .footer-button:last-of-type {
margin-right: none;
}
.modal.flyout-dialog .icon.error {
float: left;
margin-right: 10px;
width: 20px;
height: 20px;
}

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.optionsDialog-label {
width: 120px;
padding-bottom: 5px;
}
.optionsDialog-input {
width: 200px;
padding-bottom: 5px;
padding-right: 7px;
}
.options-dialog .select-box {
width: 321px;
margin-left: 1px;
}
.optionsDialog-options-groups {
padding: 15px;
height: calc(100% - 150px);
overflow-y: auto;
}
.backButtonIcon {
content: url('back.svg');
width: 20px;
margin-right: 10px;
float: left;
cursor: pointer;
}
.vs-dark.monaco-shell .backButtonIcon {
content: url('back_inverse.svg');
}
.optionsDialog-description {
padding: 15px;
overflow-y: auto;
}
.optionsDialog-options {
height: calc(100% - 30px);
}
.optionsDialog-description-content {
padding: 10px;
}
.optionsDialog-table{
width:100%;
padding: 10px;
}
td.optionsDialog-input.select-container {
padding-right: 9px;
}

View File

@@ -0,0 +1,432 @@
/*---------------------------------------------------------------------------------------------
* 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!sql/media/icons/common-icons';
import 'vs/css!./media/modal';
import { IThemable } from 'vs/platform/theme/common/styler';
import { Color } from 'vs/base/common/color';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { KeyCode } from 'vs/base/common/keyCodes';
import { mixin } from 'vs/base/common/objects';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Builder, $, withElementById } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { generateUuid } from 'vs/base/common/uuid';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
export const MODAL_SHOWING_KEY = 'modalShowing';
export const MODAL_SHOWING_CONTEXT = new RawContextKey<Array<string>>(MODAL_SHOWING_KEY, []);
export interface IModalDialogStyles {
dialogForeground?: Color;
dialogBorder?: Color;
dialogHeaderAndFooterBackground?: Color;
dialogBodyBackground?: Color;
}
export interface IModalOptions {
isFlyout?: boolean;
isWide?: boolean;
isAngular?: boolean;
hasBackButton?: boolean;
hasTitleIcon?: boolean;
hasErrors?: boolean;
hasSpinner?: boolean;
}
// Needed for angular component dialogs to style modal footer
export class ModalFooterStyle {
public static backgroundColor;
public static borderTopWidth;
public static borderTopStyle;
public static borderTopColor;
}
const defaultOptions: IModalOptions = {
isFlyout: true,
isWide: false,
isAngular: false,
hasBackButton: false,
hasTitleIcon: false,
hasErrors: false,
hasSpinner: false
};
export abstract class Modal extends Disposable implements IThemable {
private _errorMessage: Builder;
private _spinnerElement: HTMLElement;
private _errorIconElement: HTMLElement;
private _dialogForeground: Color;
private _dialogBorder: Color;
private _dialogHeaderAndFooterBackground: Color;
private _dialogBodyBackground: Color;
private _modalDialog: Builder;
private _modalHeaderSection: Builder;
private _modalBodySection: HTMLElement;
private _modalFooterSection: Builder;
private _closeButtonInHeader: Builder;
private _builder: Builder;
private _footerBuilder: Builder;
private _modalTitle: Builder;
private _modalTitleIcon: HTMLElement;
private _leftFooter: Builder;
private _rightFooter: Builder;
private _footerButtons: Button[];
private _keydownListener: IDisposable;
private _resizeListener: IDisposable;
private _modalOptions: IModalOptions;
private _backButton: Button;
private _modalShowingContext: IContextKey<Array<string>>;
private readonly _staticKey: string;
/**
* Get the back button, only available after render and if the hasBackButton option is true
*/
protected get backButton(): Button {
return this._backButton;
}
/**
* Set the dialog to have wide layout dynamically.
* Temporary solution to render file browser as wide or narrow layout.
* This will be removed once backup dialog is changed to wide layout.
*/
public setWide(isWide: boolean): void {
if (this._builder.hasClass('wide') && isWide === false) {
this._builder.removeClass('wide');
} else if (!this._builder.hasClass('wide') && isWide === true) {
this._builder.addClass('wide');
}
}
/**
* Constructor for modal
* @param _title Title of the modal, if undefined, the title section is not rendered
* @param _name Name of the modal, used for telemetry
* @param _partService
* @param options Modal options
*/
constructor(
private _title: string,
private _name: string,
private _partService: IPartService,
private _telemetryService: ITelemetryService,
private _contextKeyService: IContextKeyService,
options?: IModalOptions
) {
super();
this._modalOptions = options || Object.create(null);
mixin(this._modalOptions, defaultOptions, false);
this._staticKey = generateUuid();
this._modalShowingContext = MODAL_SHOWING_CONTEXT.bindTo(_contextKeyService);
this._footerButtons = [];
}
/**
* Build and render the modal, will call {@link Modal#renderBody}
*/
public render() {
let modalBodyClass = (this._modalOptions.isAngular === false ? 'modal-body' : 'modal-body-and-footer');
let parts: Array<HTMLElement> = [];
// This modal header section refers to the header of of the dialog
// will not be rendered if the title is passed in as undefined
if (this._title !== undefined) {
this._modalHeaderSection = $().div({ class: 'modal-header' }, (modalHeader) => {
if (this._modalOptions.hasBackButton) {
modalHeader.div({ class: 'modal-go-back' }, (cellContainer) => {
this._backButton = new Button(cellContainer);
this._backButton.icon = 'backButtonIcon';
});
}
if (this._modalOptions.hasTitleIcon) {
modalHeader.div({ class: 'modal-title-icon' }, (modalIcon) => {
this._modalTitleIcon = modalIcon.getHTMLElement();
});
}
modalHeader.div({ class: 'modal-title' }, (modalTitle) => {
this._modalTitle = modalTitle;
modalTitle.innerHtml(this._title);
});
});
parts.push(this._modalHeaderSection.getHTMLElement());
}
// This modal body section refers to the body of of the dialog
let body: Builder;
$().div({ class: modalBodyClass }, (builder) => {
body = builder;
});
this._modalBodySection = body.getHTMLElement();
parts.push(body.getHTMLElement());
this.renderBody(body.getHTMLElement());
if (this._modalOptions.isAngular === false && this._modalOptions.hasErrors) {
body.div({ class: 'dialogErrorMessage', id: 'dialogErrorMessage' }, (errorMessageContainer) => {
errorMessageContainer.div({ class: 'icon error' }, (iconContainer) => {
this._errorIconElement = iconContainer.getHTMLElement();
this._errorIconElement.style.visibility = 'hidden';
});
errorMessageContainer.div({ class: 'errorMessage' }, (messageContainer) => {
this._errorMessage = messageContainer;
});
});
}
// This modal footer section refers to the footer of of the dialog
if (this._modalOptions.isAngular === false) {
this._modalFooterSection = $().div({ class: 'modal-footer' }, (modelFooter) => {
if (this._modalOptions.hasSpinner) {
modelFooter.div({ 'class': 'icon in-progress' }, (spinnerContainer) => {
this._spinnerElement = spinnerContainer.getHTMLElement();
this._spinnerElement.style.visibility = 'hidden';
});
}
modelFooter.div({ 'class': 'left-footer' }, (leftFooter) => {
this._leftFooter = leftFooter;
});
modelFooter.div({ 'class': 'right-footer' }, (rightFooter) => {
this._rightFooter = rightFooter;
});
this._footerBuilder = modelFooter;
});
parts.push(this._modalFooterSection.getHTMLElement());
}
let builderClass = 'modal fade';
if (this._modalOptions.isFlyout) {
builderClass += ' flyout-dialog';
}
if (this._modalOptions.isWide) {
builderClass += ' wide';
}
// The builder builds the dialog. It append header, body and footer sections.
this._builder = $().div({ class: builderClass, 'role': 'dialog' }, (dialogContainer) => {
this._modalDialog = dialogContainer.div({ class: 'modal-dialog ', role: 'document' }, (modalDialog) => {
modalDialog.div({ class: 'modal-content' }, (modelContent) => {
parts.forEach((part) => {
modelContent.append(part);
});
});
});
});
}
/**
* Called for extended classes to render the body
* @param container The parent container to attach the rendered body to
*/
protected abstract renderBody(container: HTMLElement): void;
/**
* Overridable to change behavior of escape key
*/
protected onClose(e: StandardKeyboardEvent) {
this.hide();
}
/**
* Overridable to change behavior of enter key
*/
protected onAccept(e: StandardKeyboardEvent) {
this.hide();
}
/**
* Shows the modal and attaches key listeners
*/
protected show() {
this._modalShowingContext.get().push(this._staticKey);
this._builder.appendTo(withElementById(this._partService.getWorkbenchElementId()).getHTMLElement().parentElement);
this._keydownListener = DOM.addDisposableListener(document, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let context = this._modalShowingContext.get();
if (context[context.length - 1] === this._staticKey) {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter)) {
this.onAccept(event);
} else if (event.equals(KeyCode.Escape)) {
this.onClose(event);
}
}
});
this._resizeListener = DOM.addDisposableListener(window, DOM.EventType.RESIZE, (e: Event) => {
this.layout(DOM.getTotalHeight(this._builder.getHTMLElement()));
});
this.layout(DOM.getTotalHeight(this._builder.getHTMLElement()));
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ModalDialogOpened, { name: this._name });
}
/**
* Required to be implemented so that scrolling and other functions operate correctly. Should re-layout controls in the modal
*/
protected abstract layout(height?: number): void;
/**
* Hides the modal and removes key listeners
*/
protected hide() {
this._footerButtons.forEach(button => button.applyStyles());
this._modalShowingContext.get().pop();
this._builder.offDOM();
this._keydownListener.dispose();
this._resizeListener.dispose();
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ModalDialogClosed, { name: this._name });
}
/**
* Adds a button to the footer of the modal
* @param label Label to show on the button
* @param onSelect The callback to call when the button is selected
*/
protected addFooterButton(label: string, onSelect: () => void, orientation: 'left' | 'right' = 'right'): Button {
let footerButton = $('div.footer-button');
let button = new Button(footerButton);
button.label = label;
button.addListener('click', () => onSelect());
if (orientation === 'left') {
footerButton.appendTo(this._leftFooter);
} else {
footerButton.appendTo(this._rightFooter);
}
this._footerButtons.push(button);
return button;
}
/**
* Show an error in the error message element
* @param err Text to show in the error message
*/
protected setError(err: string) {
if (this._modalOptions.hasErrors) {
if (err === '') {
this._errorIconElement.style.visibility = 'hidden';
} else {
this._errorIconElement.style.visibility = 'visible';
}
this._errorMessage.innerHtml(err);
}
}
/**
* Show the spinner element that shows something is happening, hidden by default
*/
protected showSpinner(): void {
if (this._modalOptions.hasSpinner) {
this._spinnerElement.style.visibility = 'visible';
}
}
/**
* Hide the spinner element to show that something was happening, hidden by default
*/
protected hideSpinner(): void {
if (this._modalOptions.hasSpinner) {
this._spinnerElement.style.visibility = 'hidden';
}
}
/**
* Set spinner element to show or hide
*/
public set spinner(show: boolean) {
if (show) {
this.showSpinner();
} else {
this.hideSpinner();
}
}
/**
* Return background color of header and footer
*/
protected get headerAndFooterBackground(): string {
return this._dialogHeaderAndFooterBackground ? this._dialogHeaderAndFooterBackground.toString() : null;
}
/**
* Set the title of the modal
* @param title
*/
protected set title(title: string) {
if (this._title !== undefined) {
this._modalTitle.innerHtml(title);
}
}
/**
* Set the icon title class name
* @param iconClassName
*/
protected set titleIconClassName(iconClassName: string) {
if (this._modalTitleIcon) {
this._modalTitleIcon.className = 'modal-title-icon ' + iconClassName;
}
}
/**
* Called by the theme registry on theme change to style the component
*/
public style(styles: IModalDialogStyles): void {
this._dialogForeground = styles.dialogForeground;
this._dialogBorder = styles.dialogBorder;
this._dialogHeaderAndFooterBackground = styles.dialogHeaderAndFooterBackground;
this._dialogBodyBackground = styles.dialogBodyBackground;
this.applyStyles();
}
private applyStyles(): void {
const foreground = this._dialogForeground ? this._dialogForeground.toString() : null;
const border = this._dialogBorder ? this._dialogBorder.toString() : null;
const headerAndFooterBackground = this._dialogHeaderAndFooterBackground ? this._dialogHeaderAndFooterBackground.toString() : null;
const bodyBackground = this._dialogBodyBackground ? this._dialogBodyBackground.toString() : null;
ModalFooterStyle.backgroundColor = headerAndFooterBackground;
ModalFooterStyle.borderTopWidth = border ? '1px' : null;
ModalFooterStyle.borderTopStyle = border ? 'solid' : null;
ModalFooterStyle.borderTopColor = border;
if (this._closeButtonInHeader) {
this._closeButtonInHeader.style('color', foreground);
}
if (this._modalDialog) {
this._modalDialog.style('color', foreground);
this._modalDialog.style('border-width', border ? '1px' : null);
this._modalDialog.style('border-style', border ? 'solid' : null);
this._modalDialog.style('border-color', border);
}
if (this._modalHeaderSection) {
this._modalHeaderSection.style('background-color', headerAndFooterBackground);
this._modalHeaderSection.style('border-bottom-width', border ? '1px' : null);
this._modalHeaderSection.style('border-bottom-style', border ? 'solid' : null);
this._modalHeaderSection.style('border-bottom-color', border);
}
if (this._modalBodySection) {
this._modalBodySection.style.backgroundColor = bodyBackground;
}
if (this._modalFooterSection) {
this._modalFooterSection.style('background-color', ModalFooterStyle.backgroundColor);
this._modalFooterSection.style('border-top-width', ModalFooterStyle.borderTopWidth);
this._modalFooterSection.style('border-top-style', ModalFooterStyle.borderTopStyle);
this._modalFooterSection.style('border-top-color', ModalFooterStyle.borderTopColor);
}
}
public dispose() {
super.dispose();
this._keydownListener.dispose();
}
}

View File

@@ -0,0 +1,268 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/optionsDialog';
import { FixedCollapsibleView } from 'sql/platform/views/fixedCollapsibleView';
import * as DialogHelper from './dialogHelper';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IModalOptions, Modal } from './modal';
import * as OptionsDialogHelper from './optionsDialogHelper';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import * as data from 'data';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import Event, { Emitter } from 'vs/base/common/event';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { localize } from 'vs/nls';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import * as styler from 'vs/platform/theme/common/styler';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { SplitView, CollapsibleState } from 'vs/base/browser/ui/splitview/splitview';
import { Builder, $ } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import { Widget } from 'vs/base/browser/ui/widget';
class CategoryView extends FixedCollapsibleView {
private _treecontainer: HTMLElement;
constructor(private viewTitle: string, private _bodyContainer: HTMLElement, collapsed: boolean, initialBodySize: number, headerSize: number) {
super(
initialBodySize,
{
expandedBodySize: initialBodySize,
sizing: headerSize,
initialState: collapsed ? CollapsibleState.COLLAPSED : CollapsibleState.EXPANDED,
ariaHeaderLabel: viewTitle
});
}
public renderHeader(container: HTMLElement): void {
const titleDiv = $('div.title').appendTo(container);
$('span').text(this.viewTitle).appendTo(titleDiv);
}
public renderBody(container: HTMLElement): void {
this._treecontainer = document.createElement('div');
container.appendChild(this._treecontainer);
this._treecontainer.appendChild(this._bodyContainer);
}
public layoutBody(size: number): void {
this._treecontainer.style.height = size + 'px';
}
}
export class OptionsDialog extends Modal {
private _body: HTMLElement;
private _optionGroups: HTMLElement;
private _dividerBuilder: Builder;
private _okButton: Button;
private _closeButton: Button;
private _optionTitle: Builder;
private _optionDescription: Builder;
private _optionElements: { [optionName: string]: OptionsDialogHelper.IOptionElement } = {};
private _optionValues: { [optionName: string]: string };
private _optionRowSize = 31;
private _optionCategoryPadding = 30;
private _categoryHeaderSize = 22;
private _onOk = new Emitter<void>();
public onOk: Event<void> = this._onOk.event;
private _onCloseEvent = new Emitter<void>();
public onCloseEvent: Event<void> = this._onCloseEvent.event;
public okLabel: string = localize('ok', 'OK');
public cancelLabel: string = localize('cancel', 'Cancel');
constructor(
title: string,
name: string,
options: IModalOptions,
@IPartService partService: IPartService,
@IWorkbenchThemeService private _themeService: IWorkbenchThemeService,
@IContextViewService private _contextViewService: IContextViewService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(title, name, partService, telemetryService, contextKeyService, options);
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
if (this.backButton) {
this.backButton.addListener('click', () => this.cancel());
styler.attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND });
}
this._okButton = this.addFooterButton(this.okLabel, () => this.ok());
this._closeButton = this.addFooterButton(this.cancelLabel, () => this.cancel());
// Theme styler
styler.attachButtonStyler(this._okButton, this._themeService);
styler.attachButtonStyler(this._closeButton, this._themeService);
let self = this;
this._register(self._themeService.onDidColorThemeChange(e => self.updateTheme(e)));
self.updateTheme(self._themeService.getColorTheme());
}
protected renderBody(container: HTMLElement) {
new Builder(container).div({ class: 'optionsDialog-options' }, (bodyBuilder) => {
this._body = bodyBuilder.getHTMLElement();
});
let builder = new Builder(this._body);
builder.div({ class: 'Connection-divider' }, (dividerContainer) => {
this._dividerBuilder = dividerContainer;
});
builder.div({ class: 'optionsDialog-description' }, (descriptionContainer) => {
descriptionContainer.div({ class: 'modal-title' }, (optionTitle) => {
this._optionTitle = optionTitle;
});
descriptionContainer.div({ class: 'optionsDialog-description-content' }, (optionDescription) => {
this._optionDescription = optionDescription;
});
});
}
// Update theming that is specific to options dialog flyout body
private updateTheme(theme: IColorTheme): void {
let borderColor = theme.getColor(contrastBorder);
let border = borderColor ? borderColor.toString() : null;
if (this._dividerBuilder) {
this._dividerBuilder.style('border-top-width', border ? '1px' : null);
this._dividerBuilder.style('border-top-style', border ? 'solid' : null);
this._dividerBuilder.style('border-top-color', border);
}
}
private onOptionLinkClicked(optionName: string): void {
var option = this._optionElements[optionName].option;
this._optionTitle.innerHtml(option.displayName);
this._optionDescription.innerHtml(option.description);
}
private fillInOptions(container: Builder, options: data.ServiceOption[]): void {
for (var i = 0; i < options.length; i++) {
var option: data.ServiceOption = options[i];
var rowContainer = DialogHelper.appendRow(container, option.displayName, 'optionsDialog-label', 'optionsDialog-input');
OptionsDialogHelper.createOptionElement(option, rowContainer, this._optionValues, this._optionElements, this._contextViewService, (name) => this.onOptionLinkClicked(name));
}
}
private registerStyling(): void {
// Theme styler
for (var optionName in this._optionElements) {
var widget: Widget = this._optionElements[optionName].optionWidget;
var option = this._optionElements[optionName].option;
switch (option.valueType) {
case OptionsDialogHelper.ServiceOptionType.category:
case OptionsDialogHelper.ServiceOptionType.boolean:
this._register(styler.attachSelectBoxStyler(<SelectBox>widget, this._themeService));
break;
case OptionsDialogHelper.ServiceOptionType.string:
case OptionsDialogHelper.ServiceOptionType.password:
case OptionsDialogHelper.ServiceOptionType.number:
this._register(styler.attachInputBoxStyler(<InputBox>widget, this._themeService));
}
}
}
public get optionValues(): { [name: string]: any } {
return this._optionValues;
}
public hideError() {
this.setError('');
}
public showError(err: string) {
this.setError(err);
}
/* Overwrite escape key behavior */
protected onClose() {
this.close();
}
/* Overwrite enter key behavior */
protected onAccept() {
this.ok();
}
public ok(): void {
if (OptionsDialogHelper.validateInputs(this._optionElements)) {
OptionsDialogHelper.updateOptions(this._optionValues, this._optionElements);
this._onOk.fire();
this.close();
}
}
public cancel() {
this.close();
}
public close() {
this._optionGroups.remove();
this.dispose();
this.hide();
this._onCloseEvent.fire();
}
public open(options: data.ServiceOption[], optionValues: { [name: string]: any }) {
this._optionValues = optionValues;
var firstOption: string;
var containerGroup: Builder;
var layoutSize = 0;
var optionsContentBuilder: Builder = $().div({ class: 'optionsDialog-options-groups' }, (container) => {
containerGroup = container;
this._optionGroups = container.getHTMLElement();
});
var splitview = new SplitView(containerGroup.getHTMLElement());
let categoryMap = OptionsDialogHelper.groupOptionsByCategory(options);
for (var category in categoryMap) {
var serviceOptions: data.ServiceOption[] = categoryMap[category];
var bodyContainer = $().element('table', { class: 'optionsDialog-table' }, (tableContainer: Builder) => {
this.fillInOptions(tableContainer, serviceOptions);
});
var viewSize = this._optionCategoryPadding + serviceOptions.length * this._optionRowSize;
layoutSize += (viewSize + this._categoryHeaderSize);
var categoryView = new CategoryView(category, bodyContainer.getHTMLElement(), false, viewSize, this._categoryHeaderSize);
splitview.addView(categoryView);
if (!firstOption) {
firstOption = serviceOptions[0].name;
}
}
splitview.layout(layoutSize);
let body = new Builder(this._body);
body.append(optionsContentBuilder.getHTMLElement(), 0);
this.show();
var firstOptionWidget = this._optionElements[firstOption].optionWidget;
this.registerStyling();
firstOptionWidget.focus();
}
protected layout(height?: number): void {
// Nothing currently laid out in this class
}
public dispose(): void {
super.dispose();
for (var optionName in this._optionElements) {
var widget: Widget = this._optionElements[optionName].optionWidget;
widget.dispose();
delete this._optionElements[optionName];
}
}
}

View File

@@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as DialogHelper from './dialogHelper';
import { Builder } from 'vs/base/browser/builder';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import * as types from 'vs/base/common/types';
import data = require('data');
import { localize } from 'vs/nls';
export interface IOptionElement {
optionWidget: any;
option: data.ServiceOption;
optionValue: any;
}
export enum ServiceOptionType {
string = 0,
multistring = 1,
password = 2,
number = 3,
category = 4,
boolean = 5
}
export function createOptionElement(option: data.ServiceOption, rowContainer: Builder, options: { [name: string]: any },
optionsMap: { [optionName: string]: IOptionElement }, contextViewService: IContextViewService, onFocus: (name) => void): void {
let possibleInputs: string[] = [];
let optionValue = this.getOptionValueAndCategoryValues(option, options, possibleInputs);
let optionWidget: any;
let inputElement: HTMLElement;
let missingErrorMessage = localize('missingRequireField', ' is required.');
let invalidInputMessage = localize('invalidInput', 'Invalid input. Numeric value expected.');
switch (option.valueType) {
case ServiceOptionType.number:
optionWidget = new InputBox(rowContainer.getHTMLElement(), contextViewService, {
validationOptions: {
validation: (value: string) => {
if (!value && option.isRequired) {
return { type: MessageType.ERROR, content: option.displayName + missingErrorMessage };
} else if (!types.isNumber(Number(value))) {
return { type: MessageType.ERROR, content: invalidInputMessage };
} else {
return null;
}
}
}
});
optionWidget.value = optionValue;
inputElement = this.findElement(rowContainer, 'input');
break;
case ServiceOptionType.category:
case ServiceOptionType.boolean:
optionWidget = new SelectBox(possibleInputs, optionValue.toString());
DialogHelper.appendInputSelectBox(rowContainer, optionWidget);
inputElement = this.findElement(rowContainer, 'select-box');
break;
case ServiceOptionType.string:
case ServiceOptionType.password:
optionWidget = new InputBox(rowContainer.getHTMLElement(), contextViewService, {
validationOptions: {
validation: (value: string) => (!value && option.isRequired) ? ({ type: MessageType.ERROR, content: option.displayName + missingErrorMessage }) : null
}
});
optionWidget.value = optionValue;
if (option.valueType === ServiceOptionType.password) {
optionWidget.inputElement.type = 'password';
}
inputElement = this.findElement(rowContainer, 'input');
}
optionsMap[option.name] = { optionWidget: optionWidget, option: option, optionValue: optionValue };
inputElement.onfocus = () => onFocus(option.name);
}
export function getOptionValueAndCategoryValues(option: data.ServiceOption, options: { [optionName: string]: any }, possibleInputs: string[]): any {
var optionValue = option.defaultValue;
if (options[option.name]) {
// if the value type is boolean, the option value can be either boolean or string
if (option.valueType === ServiceOptionType.boolean) {
if (options[option.name] === true || options[option.name] === this.trueInputValue) {
optionValue = this.trueInputValue;
} else {
optionValue = this.falseInputValue;
}
} else {
optionValue = options[option.name];
}
}
if (option.valueType === ServiceOptionType.boolean || option.valueType === ServiceOptionType.category) {
// If the option is not required, the empty string should be add at the top of possible choices
if (!option.isRequired) {
possibleInputs.push('');
}
if (option.valueType === ServiceOptionType.boolean) {
possibleInputs.push(this.trueInputValue, this.falseInputValue);
} else {
option.categoryValues.map(c => possibleInputs.push(c.name));
}
// If the option value is not set and default value is null, the option value should be set to the first possible input.
if (optionValue === null || optionValue === undefined) {
optionValue = possibleInputs[0];
}
}
return optionValue;
}
export function validateInputs(optionsMap: { [optionName: string]: IOptionElement }): boolean {
let isValid = true;
let isFocused = false;
for (var optionName in optionsMap) {
var optionElement: IOptionElement = optionsMap[optionName];
var widget = optionElement.optionWidget;
var isInputBox = (optionElement.option.valueType === ServiceOptionType.string ||
optionElement.option.valueType === ServiceOptionType.password ||
optionElement.option.valueType === ServiceOptionType.number);
if (isInputBox) {
if (!widget.validate()) {
isValid = false;
if (!isFocused) {
isFocused = true;
widget.focus();
}
}
}
}
return isValid;
}
export function updateOptions(options: { [optionName: string]: any }, optionsMap: { [optionName: string]: IOptionElement }): void {
for (var optionName in optionsMap) {
var optionElement: IOptionElement = optionsMap[optionName];
if (optionElement.optionWidget.value !== options[optionName]) {
if (!optionElement.optionWidget.value && options[optionName]) {
delete options[optionName];
}
if (optionElement.optionWidget.value) {
if (optionElement.option.valueType === ServiceOptionType.boolean) {
options[optionName] = (optionElement.optionWidget.value === this.trueInputValue) ? true : false;
} else {
options[optionName] = optionElement.optionWidget.value;
}
}
optionElement.optionValue = options[optionName];
}
}
}
export let trueInputValue: string = 'True';
export let falseInputValue: string = 'False';
export function findElement(container: Builder, className: string): HTMLElement {
var elementBuilder: Builder = container;
while (elementBuilder.getHTMLElement()) {
var htmlElement = elementBuilder.getHTMLElement();
if (htmlElement.className === className) {
break;
}
elementBuilder = elementBuilder.child(0);
}
return elementBuilder.getHTMLElement();
}
export function groupOptionsByCategory(options: data.ServiceOption[]): { [category: string]: data.ServiceOption[] } {
var connectionOptionsMap: { [category: string]: data.ServiceOption[] } = {};
options.forEach(option => {
var groupName = option.groupName;
if (groupName === null || groupName === undefined) {
groupName = 'General';
}
if (!!connectionOptionsMap[groupName]) {
connectionOptionsMap[groupName].push(option);
} else {
connectionOptionsMap[groupName] = [option];
}
});
return connectionOptionsMap;
}