diff --git a/extensions/machine-learning/src/common/apiWrapper.ts b/extensions/machine-learning/src/common/apiWrapper.ts index 7cf289614e..4881624737 100644 --- a/extensions/machine-learning/src/common/apiWrapper.ts +++ b/extensions/machine-learning/src/common/apiWrapper.ts @@ -79,8 +79,8 @@ export class ApiWrapper { return azdata.window.createTab(title); } - public createModelViewDialog(title: string, dialogName?: string, isWide?: boolean): azdata.window.Dialog { - return azdata.window.createModelViewDialog(title, dialogName, isWide); + public createModelViewDialog(title: string, dialogName?: string, width?: azdata.window.DialogWidth, dialogStyle?: azdata.window.DialogStyle, dialogPosition?: azdata.window.DialogPosition, renderHeader?: boolean, renderFooter?: boolean, dialogProperties?: azdata.window.IDialogProperties): azdata.window.Dialog { + return azdata.window.createModelViewDialog(title, dialogName, width, dialogStyle, dialogPosition, renderHeader, renderFooter, dialogProperties); } public createWizard(title: string): azdata.window.Wizard { diff --git a/extensions/machine-learning/src/common/constants.ts b/extensions/machine-learning/src/common/constants.ts index 4e60780307..6be485b4dd 100644 --- a/extensions/machine-learning/src/common/constants.ts +++ b/extensions/machine-learning/src/common/constants.ts @@ -244,8 +244,13 @@ export const invalidModelToPredictError = localize('models.invalidModelToPredict export const invalidModelParametersError = localize('models.invalidModelParametersError', "Please select valid source table and model parameters"); export const invalidModelToSelectError = localize('models.invalidModelToSelectError', "Please select a valid model"); export const invalidModelImportTargetError = localize('models.invalidModelImportTargetError', "Please select a valid table"); + +export const columnDataTypeMismatchWarningHelper = localize('models.columnDataTypeMismatchWarningHelper', "Click to review warning details"); +export const columnDataTypeMismatchWarningHeading = localize('models.columnDataTypeMismatchWarningHeading', "Differences in data type"); export const columnDataTypeMismatchWarning = localize('models.columnDataTypeMismatchWarning', "The data type of the source table column does not match the required input field’s type."); export const outputColumnDataTypeNotSupportedWarning = localize('models.outputColumnDataTypeNotSupportedWarning', "The data type of output column does not match the output field’s type."); + + export const modelNameRequiredError = localize('models.modelNameRequiredError', "Model name is required."); export const modelsRequiredError = localize('models.modelsRequiredError', "Please select at least one model to import."); export const updateModelFailedError = localize('models.updateModelFailedError', "Failed to update the model"); diff --git a/extensions/machine-learning/src/views/models/manageModels/manageModelsDialog.ts b/extensions/machine-learning/src/views/models/manageModels/manageModelsDialog.ts index 704524783a..edf2964702 100644 --- a/extensions/machine-learning/src/views/models/manageModels/manageModelsDialog.ts +++ b/extensions/machine-learning/src/views/models/manageModels/manageModelsDialog.ts @@ -41,6 +41,7 @@ export class ManageModelsDialog extends ModelViewBase { let dialog = this.dialogView.createDialog(constants.viewImportModelsTitle, [this.currentLanguagesTab]); dialog.isWide = true; + dialog.dialogStyle = 'flyout'; dialog.customButtons = [registerModelButton]; this.mainViewPanel = dialog; dialog.okButton.hidden = true; diff --git a/extensions/machine-learning/src/views/models/prediction/columnsTable.ts b/extensions/machine-learning/src/views/models/prediction/columnsTable.ts index f1d2c93665..cc84cb4158 100644 --- a/extensions/machine-learning/src/views/models/prediction/columnsTable.ts +++ b/extensions/machine-learning/src/views/models/prediction/columnsTable.ts @@ -11,16 +11,19 @@ import { IDataComponent } from '../../interfaces'; import { PredictColumn, DatabaseTable, TableColumn } from '../../../prediction/interfaces'; import { ModelParameter, ModelParameters } from '../../../modelManagement/interfaces'; +const WarningButtonDimensions = { + height: 16, + width: 16 +}; + /** * View to render azure models in a table */ export class ColumnsTable extends ModelViewBase implements IDataComponent { - private _table: azdata.DeclarativeTableComponent | undefined; private _parameters: PredictColumn[] = []; private _loader: azdata.LoadingComponent; - /** * Creates a view to render azure models in a table */ @@ -205,8 +208,15 @@ export class ColumnsTable extends ModelViewBase implements IDataComponent { + let warningButtonProperties = { + xPos: 0, + yPos: 0, + width: WarningButtonDimensions.width, + height: WarningButtonDimensions.height + }; + this.openWarningCalloutDialog(constants.columnDataTypeMismatchWarningHeading, 'output-table-row-dialog', constants.outputColumnDataTypeNotSupportedWarning, constants.learnMoreLink, constants.mlExtDocLink, warningButtonProperties); }); const css = { 'padding-top': '5px', @@ -298,8 +308,15 @@ export class ColumnsTable extends ModelViewBase implements IDataComponent { + let warningButtonProperties = { + xPos: 0, + yPos: 0, + width: WarningButtonDimensions.width, + height: WarningButtonDimensions.height + }; + this.openWarningCalloutDialog(constants.columnDataTypeMismatchWarningHeading, 'input-table-row-dialog', constants.columnDataTypeMismatchWarning, constants.learnMoreLink, constants.mlExtDocLink, warningButtonProperties); }); const css = { @@ -341,7 +358,7 @@ export class ColumnsTable extends ModelViewBase implements IDataComponent { + const warningContentContainer = view.modelBuilder.divContainer().withProperties({}).component(); + const messageTextComponent = view.modelBuilder.text().withProperties({ + value: calloutMessageText, + CSSStyles: { + 'font-size': '12px', + 'line-height': '16px', + 'margin': '0 0 12px 0' + } + }).component(); + warningContentContainer.addItem(messageTextComponent); + + if (calloutMessageLinkText && calloutMessageLinkUrl) { + const messageLinkComponent = view.modelBuilder.hyperlink().withProperties({ + label: calloutMessageLinkText, + url: calloutMessageLinkUrl, + CSSStyles: { + 'font-size': '13px', + 'margin': '0px' + } + }).component(); + warningContentContainer.addItem(messageLinkComponent); + } + view.initializeModel(warningContentContainer); + }); + // set tab as content + dialog.content = [warningTab]; + + azdata.window.openDialog(dialog); + } + /** * Returns selected data */ diff --git a/extensions/machine-learning/src/views/models/tableSelectionComponent.ts b/extensions/machine-learning/src/views/models/tableSelectionComponent.ts index aba7ea516d..6947b69813 100644 --- a/extensions/machine-learning/src/views/models/tableSelectionComponent.ts +++ b/extensions/machine-learning/src/views/models/tableSelectionComponent.ts @@ -285,7 +285,6 @@ export class TableSelectionComponent extends ModelViewBase implements IDataCompo } await this.onTableSelected(); - } private refreshTableComponent(): void { diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 4902876d45..6cf0e9be24 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -661,9 +661,35 @@ declare module 'azdata' { export interface Dialog { /** - * Width of the dialog + * Width of the dialog. + * Default is 'narrrow'. */ width?: DialogWidth; + /** + * Dialog style type: normal, flyout, callout. + * Default is 'flyout'. + */ + dialogStyle?: DialogStyle; + /** + * Dialog position type: left, below and undefined. + * Default is undefined. + */ + dialogPosition?: DialogPosition; + /** + * Specify whether or not to render the Dialog header. + * Default is true. + */ + renderHeader?: boolean; + /** + * Specify whether or not to render the Dialog footer. + * Default is true. + */ + renderFooter?: boolean; + /** + * Positional data prior to opening of dialog. + * Default is undefined. + */ + dialogProperties?: IDialogProperties; } export interface Wizard { @@ -687,12 +713,39 @@ declare module 'azdata' { export type DialogWidth = 'narrow' | 'medium' | 'wide' | number; /** - * Create a dialog with the given title - * @param title The title of the dialog, displayed at the top - * @param dialogName the name of the dialog - * @param width width of the dialog, default is 'wide' + * These dialog styles affect how the dialog dispalys in the application. + * normal: Positioned top and centered. + * flyout (default): Existing panel appearance - positioned full screen height, opens from the right side of the application. + * callout: Opens below or beside button clicked, contains footer section with buttons. */ - export function createModelViewDialog(title: string, dialogName?: string, width?: DialogWidth): Dialog; + export type DialogStyle = 'normal' | 'flyout' | 'callout'; + + export type DialogPosition = 'left' | 'below'; + + /** + * These are positional data prior to opening of dialog. + * They are needed for positioning relative to the button which triggers the opening of the dialog. + * Default is undefined. + */ + export interface IDialogProperties { + xPos: number, + yPos: number, + width: number, + height: number + } + + /** + * Create a dialog with the given title + * @param title Title of the dialog, displayed at the top. + * @param dialogName Name of the dialog. + * @param width Width of the dialog, default is 'narrow'. + * @param dialogStyle Defines the dialog style, default is 'flyout'. + * @param dialogPosition Defines the dialog position, default is undefined + * @param renderHeader Specify whether or not to render the Dialog header, default is true. + * @param renderFooter Specify whether or not to render the Dialog footer, default is true. + * @param dialogProperties Positional data prior to opening of dialog, default is undefined. + */ + export function createModelViewDialog(title: string, dialogName?: string, width?: DialogWidth, dialogStyle?: DialogStyle, dialogPosition?: DialogPosition, renderHeader?: boolean, renderFooter?: boolean, dialogProperties?: IDialogProperties): Dialog; /** * Create a wizard with the given title and width diff --git a/src/sql/media/icons/toolbar-image.svg b/src/sql/media/icons/toolbar-image.svg index df9c8276b0..a1504721b5 100644 --- a/src/sql/media/icons/toolbar-image.svg +++ b/src/sql/media/icons/toolbar-image.svg @@ -1,4 +1,4 @@ - -add inline image - + +insert image + diff --git a/src/sql/platform/telemetry/common/telemetryKeys.ts b/src/sql/platform/telemetry/common/telemetryKeys.ts index cc5bdea493..da1290ff38 100644 --- a/src/sql/platform/telemetry/common/telemetryKeys.ts +++ b/src/sql/platform/telemetry/common/telemetryKeys.ts @@ -43,6 +43,7 @@ export const FireWallRule = 'FirewallRule'; export const AutoOAuth = 'AutoOAuth'; export const AddNewDashboardTab = 'AddNewDashboardTab'; export const ProfilerFilter = 'ProfilerFilter'; +export const CalloutDialog = 'CalloutDialog'; export enum TelemetryView { diff --git a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts index d5410da1d9..0072dcae1e 100644 --- a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts @@ -75,6 +75,11 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape let dialog = this.getDialog(handle); const options = assign({}, DefaultDialogOptions); options.width = dialog.width; + options.dialogStyle = dialog.dialogStyle; + options.dialogPosition = dialog.dialogPosition; + options.renderHeader = dialog.renderHeader; + options.renderFooter = dialog.renderFooter; + options.dialogProperties = dialog.dialogProperties; this._dialogService.showDialog(dialog, dialogName, options); return Promise.resolve(); } @@ -88,11 +93,16 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape public $setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable { let dialog = this._dialogs.get(handle); if (!dialog) { - dialog = new Dialog(details.title, details.width); - let okButton = this.getButton(details.okButton); - let cancelButton = this.getButton(details.cancelButton); - dialog.okButton = okButton; - dialog.cancelButton = cancelButton; + dialog = new Dialog(details.title, details.width, details.dialogStyle, details.dialogPosition, details.renderHeader, details.renderFooter, details.dialogProperties); + + /** + * Only peform actions on footer if it is shown. + */ + if (details.renderFooter !== false) { + dialog.okButton = this.getButton(details.okButton); + dialog.cancelButton = this.getButton(details.cancelButton); + } + dialog.onValidityChanged(valid => this._proxy.$onPanelValidityChanged(handle, valid)); dialog.registerCloseValidator(() => this.validateDialogClose(handle)); this._dialogs.set(handle, dialog); @@ -110,7 +120,6 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape if (details.customButtons) { dialog.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle)); } - dialog.message = details.message; return Promise.resolve(); diff --git a/src/sql/workbench/api/common/extHostModelViewDialog.ts b/src/sql/workbench/api/common/extHostModelViewDialog.ts index b54bfc654e..26fb40625c 100644 --- a/src/sql/workbench/api/common/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/common/extHostModelViewDialog.ts @@ -13,7 +13,7 @@ import * as azdata from 'azdata'; import { SqlMainContext, ExtHostModelViewDialogShape, MainThreadModelViewDialogShape, ExtHostModelViewShape, ExtHostBackgroundTaskManagementShape } from 'sql/workbench/api/common/sqlExtHost.protocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { TabOrientation, DialogWidth } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { TabOrientation, DialogWidth, DialogStyle, DialogPosition, IDialogProperties } from 'sql/workbench/api/common/sqlExtHostTypes'; const DONE_LABEL = nls.localize('dialogDoneLabel', "Done"); const CANCEL_LABEL = nls.localize('dialogCancelLabel', "Cancel"); @@ -127,6 +127,11 @@ class DialogImpl extends ModelViewPanelImpl implements azdata.window.Dialog { private _dialogName: string; private _isWide: boolean; private _width: DialogWidth; + private _dialogStyle: DialogStyle; + private _dialogPosition: DialogPosition; + private _renderHeader: boolean; + private _renderFooter: boolean; + private _dialogProperties: IDialogProperties; constructor(extHostModelViewDialog: ExtHostModelViewDialog, extHostModelView: ExtHostModelViewShape, @@ -141,6 +146,46 @@ class DialogImpl extends ModelViewPanelImpl implements azdata.window.Dialog { }); } + public get dialogStyle(): azdata.window.DialogStyle { + return this._dialogStyle; + } + + public set dialogStyle(value: azdata.window.DialogStyle) { + this._dialogStyle = value; + } + + public get dialogPosition(): azdata.window.DialogPosition { + return this._dialogPosition; + } + + public set dialogPosition(value: azdata.window.DialogPosition) { + this._dialogPosition = value; + } + + public get renderHeader(): boolean { + return this._renderHeader; + } + + public set renderHeader(value: boolean) { + this._renderHeader = value; + } + + public get renderFooter(): boolean { + return this._renderFooter; + } + + public set renderFooter(value: boolean) { + this._renderFooter = value; + } + + public get dialogProperties(): IDialogProperties { + return this._dialogProperties; + } + + public set dialogProperties(value: IDialogProperties) { + this._dialogProperties = value; + } + public get width(): azdata.window.DialogWidth { return this._width; } @@ -674,20 +719,49 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { } public updateDialogContent(dialog: azdata.window.Dialog): void { + let dialogWidth: DialogWidth = 'narrow'; + let dialogStyle: DialogStyle; + let dialogPosition: DialogPosition; + let renderHeader: boolean; + let renderFooter: boolean; + let dialogProperties: IDialogProperties; let handle = this.getHandle(dialog); let tabs = dialog.content; + + if (dialog.dialogStyle) { + dialogStyle = dialog.dialogStyle; + } + if (dialog.dialogPosition) { + dialogPosition = dialog.dialogPosition; + } + if (dialog.renderHeader) { + renderHeader = dialog.renderHeader; + } + if (dialog.renderFooter) { + renderFooter = dialog.renderFooter; + } + if (dialog.dialogProperties) { + dialogProperties = dialog.dialogProperties; + } if (tabs && typeof tabs !== 'string') { tabs.forEach(tab => this.updateTabContent(tab)); } + if (dialog.customButtons) { dialog.customButtons.forEach(button => { button.secondary = true; this.updateButton(button); }); } - this.updateButton(dialog.okButton); - this.updateButton(dialog.cancelButton); - let dialogWidth: DialogWidth = 'narrow'; + + /** + * Only peform actions on footer if it is shown. + */ + if (dialog.renderFooter !== false) { + this.updateButton(dialog.okButton); + this.updateButton(dialog.cancelButton); + } + if (dialog.isWide !== undefined) { dialogWidth = dialog.isWide ? 'wide' : 'narrow'; } else if (dialog.width !== undefined) { @@ -698,6 +772,11 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { this._proxy.$setDialogDetails(handle, { title: dialog.title, width: dialogWidth, + dialogStyle: dialogStyle, + dialogPosition: dialogPosition, + renderHeader: renderHeader, + renderFooter: renderFooter, + dialogProperties: dialogProperties, okButton: this.getHandle(dialog.okButton), cancelButton: this.getHandle(dialog.cancelButton), content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getHandle(tab)) : dialog.content as string, @@ -731,12 +810,27 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { this._onClickCallbacks.set(handle, callback); } - public createDialog(title: string, dialogName?: string, extension?: IExtensionDescription, width?: azdata.window.DialogWidth): azdata.window.Dialog { + public createDialog(title: string, dialogName?: string, extension?: IExtensionDescription, width?: DialogWidth, dialogStyle?: DialogStyle, dialogPosition?: DialogPosition, renderHeader?: boolean, renderFooter?: boolean, dialogProperties?: IDialogProperties): azdata.window.Dialog { + let dialog = new DialogImpl(this, this._extHostModelView, this._extHostTaskManagement, extension); + if (dialogName) { dialog.dialogName = dialogName; } - dialog.title = title; + if (dialogStyle) { + dialog.dialogStyle = dialogStyle; + } + if (dialogPosition) { + dialog.dialogPosition = dialogPosition; + } + if (dialogProperties) { + dialog.dialogProperties = dialogProperties; + } + dialog.renderHeader = renderHeader; + dialog.renderFooter = renderFooter; + if (title) { + dialog.title = title; + } dialog.width = width ?? 'narrow'; dialog.handle = this.getHandle(dialog); return dialog; diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index 1eb00c44de..2ac3b62845 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -141,7 +141,7 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp }, connect(connectionProfile: azdata.IConnectionProfile, saveConnection: boolean, showDashboard: boolean): Thenable { return extHostConnectionManagement.$connect(connectionProfile, saveConnection, showDashboard); - }, + } }; // Backcompat "sqlops" APIs @@ -416,14 +416,17 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp return extHostModalDialogs.createDialog(name); }, // the 'width' parameter used to be boolean type named 'isWide', the optional boolean type for 'width' parameter is added for backward compatibility support of 'isWide' parameter. - createModelViewDialog(title: string, dialogName?: string, width?: boolean | azdata.window.DialogWidth): azdata.window.Dialog { + createModelViewDialog(title: string, dialogName?: string, width?: boolean | sqlExtHostTypes.DialogWidth, dialogStyle?: sqlExtHostTypes.DialogStyle, dialogPosition?: sqlExtHostTypes.DialogPosition, renderHeader?: boolean, renderFooter?: boolean, dialogProperties?: sqlExtHostTypes.IDialogProperties): azdata.window.Dialog { let dialogWidth: azdata.window.DialogWidth; if (typeof width === 'boolean') { dialogWidth = width === true ? 'wide' : 'narrow'; } else { dialogWidth = width; } - return extHostModelViewDialog.createDialog(title, dialogName, extension, dialogWidth); + if (dialogStyle === undefined) { + dialogStyle = 'flyout'; + } + return extHostModelViewDialog.createDialog(title, dialogName, extension, dialogWidth, dialogStyle, dialogPosition, renderHeader, renderFooter, dialogProperties); }, createTab(title: string): azdata.window.DialogTab { return extHostModelViewDialog.createTab(title, extension); diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index bcf581d580..de9ebf7dbe 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -259,6 +259,11 @@ export interface IModelViewDialogDetails { customButtons: number[]; message: DialogMessage; width: DialogWidth; + dialogStyle: DialogStyle; + dialogPosition: DialogPosition; + renderHeader: boolean; + renderFooter: boolean; + dialogProperties: IDialogProperties; } export interface IModelViewTabDetails { @@ -302,6 +307,17 @@ export interface IModelViewWizardDetails { export type DialogWidth = 'narrow' | 'medium' | 'wide' | number; +export type DialogStyle = 'normal' | 'flyout' | 'callout'; + +export type DialogPosition = 'left' | 'below'; + +export interface IDialogProperties { + xPos: number, + yPos: number, + width: number, + height: number +} + export enum MessageLevel { Error = 0, Warning = 1, diff --git a/src/sql/workbench/browser/modal/calloutDialog.ts b/src/sql/workbench/browser/modal/calloutDialog.ts new file mode 100644 index 0000000000..822ca01d98 --- /dev/null +++ b/src/sql/workbench/browser/modal/calloutDialog.ts @@ -0,0 +1,326 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import * as DOM from 'vs/base/browser/dom'; +import * as styler from 'vs/platform/theme/common/styler'; +import * as strings from 'vs/base/common/strings'; +import { IDialogProperties, Modal, DialogWidth } from 'sql/workbench/browser/modal/modal'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { localize } from 'vs/nls'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IFileDialogService, IOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import { attachModalDialogStyler } from 'sql/workbench/common/styler'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { Deferred } from 'sql/base/common/promise'; +import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; +import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox'; +import { RadioButton } from 'sql/base/browser/ui/radioButton/radioButton'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; + +export type CalloutType = 'IMAGE' | 'LINK'; + +export interface ICalloutDialogOptions { + insertTitle?: string, + calloutType?: CalloutType, + insertMarkup?: string, + imagePath?: string, + embedImage?: boolean +} +export class CalloutDialog extends Modal { + private _calloutType: CalloutType; + private _selectionComplete: Deferred; + // Link + private _linkTextLabel: HTMLElement; + private _linkTextInputBox: InputBox; + private _linkAddressLabel: HTMLElement; + private _linkUrlInputBox: InputBox; + // Image + private _imageLocationLabel: HTMLElement; + private _imageLocalRadioButton: RadioButton; + private _editorImageLocationGroup: string = 'editorImageLocationGroup'; + private _imageRemoteRadioButton: RadioButton; + private _imageUrlLabel: HTMLElement; + private _imageUrlInputBox: InputBox; + private _imageBrowseButton: HTMLAnchorElement; + private _imageEmbedLabel: HTMLElement; + private _imageEmbedCheckbox: Checkbox; + + private readonly insertButtonText = localize('callout.insertButton', "Insert"); + private readonly cancelButtonText = localize('callout.cancelButton', "Cancel"); + // Link + private readonly linkTextLabel = localize('callout.linkTextLabel', "Text to display"); + private readonly linkTextPlaceholder = localize('callout.linkTextPlaceholder', "Text to display"); + private readonly linkAddressLabel = localize('callout.linkAddressLabel', "Address"); + private readonly linkAddressPlaceholder = localize('callout.linkAddressPlaceholder', "Link to an existing file or web page"); + // Image + private readonly locationLabel = localize('callout.locationLabel', "Image location"); + private readonly localImageLabel = localize('callout.localImageLabel', "This computer"); + private readonly remoteImageLabel = localize('callout.remoteImageLabel', "Online"); + private readonly pathInputLabel = localize('callout.pathInputLabel', "Image URL"); + private readonly pathPlaceholder = localize('callout.pathPlaceholder', "Enter image path"); + private readonly urlPlaceholder = localize('callout.urlPlaceholder', "Enter image URL"); + private readonly browseAltText = localize('callout.browseAltText', "Browse"); + private readonly embedImageLabel = localize('callout.embedImageLabel', "Attach image to notebook"); + + constructor( + calloutType: CalloutType, + title: string, + width: DialogWidth, + dialogProperties: IDialogProperties, + @IPathService private readonly _pathService: IPathService, + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + @IThemeService themeService: IThemeService, + @ILayoutService layoutService: ILayoutService, + @IAdsTelemetryService telemetryService: IAdsTelemetryService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextViewService private _contextViewService: IContextViewService, + @IClipboardService clipboardService: IClipboardService, + @ILogService logService: ILogService, + @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService + ) { + super( + title, + TelemetryKeys.CalloutDialog, + telemetryService, + layoutService, + clipboardService, + themeService, + logService, + textResourcePropertiesService, + contextKeyService, + { + dialogStyle: 'callout', + dialogPosition: 'below', + dialogProperties: dialogProperties, + width: width + }); + + this._selectionComplete = new Deferred(); + this._calloutType = calloutType; + } + + /** + * Opens the dialog and returns a promise for what options the user chooses. + */ + public open(): Promise { + this.show(); + return this._selectionComplete.promise; + } + + public render() { + super.render(); + + attachModalDialogStyler(this, this._themeService); + + this.addFooterButton(this.insertButtonText, () => this.insert()); + this.addFooterButton(this.cancelButtonText, () => this.cancel(), undefined, true); + + this.registerListeners(); + } + + protected renderBody(container: HTMLElement) { + if (this._calloutType === 'IMAGE') { + this.buildInsertImageCallout(container); + } + + if (this._calloutType === 'LINK') { + this.buildInsertLinkCallout(container); + } + } + + private buildInsertImageCallout(container: HTMLElement): void { + let imageContentColumn = DOM.$('.column.insert-image'); + DOM.append(container, imageContentColumn); + + let locationRow = DOM.$('.row'); + DOM.append(imageContentColumn, locationRow); + + this._imageLocationLabel = DOM.$('p'); + this._imageLocationLabel.innerText = this.locationLabel; + DOM.append(locationRow, this._imageLocationLabel); + + let radioButtonGroup = DOM.$('.radio-group'); + this._imageLocalRadioButton = new RadioButton(radioButtonGroup, { + label: this.localImageLabel, + enabled: true, + checked: true + }); + this._imageRemoteRadioButton = new RadioButton(radioButtonGroup, { + label: this.remoteImageLabel, + enabled: true, + checked: false + }); + this._imageLocalRadioButton.value = localize('local', "Local"); + this._imageLocalRadioButton.name = this._editorImageLocationGroup; + this._imageRemoteRadioButton.value = localize('remote', "Remote"); + this._imageRemoteRadioButton.name = this._editorImageLocationGroup; + DOM.append(locationRow, radioButtonGroup); + + let pathRow = DOM.$('.row'); + DOM.append(imageContentColumn, pathRow); + this._imageUrlLabel = DOM.$('p'); + if (this._imageLocalRadioButton.checked === true) { + this._imageUrlLabel.innerText = this.pathPlaceholder; + } else { + this._imageUrlLabel.innerText = this.urlPlaceholder; + } + DOM.append(pathRow, this._imageUrlLabel); + + let inputContainer = DOM.$('.flex-container'); + this._imageUrlInputBox = new InputBox( + inputContainer, + this._contextViewService, + { + placeholder: this.pathPlaceholder, + ariaLabel: this.pathInputLabel + }); + let browseButtonContainer = DOM.$('.button-icon'); + this._imageBrowseButton = DOM.$('a.codicon.masked-icon.browse-local'); + this._imageBrowseButton.title = this.browseAltText; + DOM.append(inputContainer, browseButtonContainer); + DOM.append(browseButtonContainer, this._imageBrowseButton); + + this._register(DOM.addDisposableListener(this._imageBrowseButton, DOM.EventType.CLICK, async () => { + let selectedUri = await this.handleBrowse(); + if (selectedUri) { + this._imageUrlInputBox.value = selectedUri.fsPath; + } + }, true)); + + this._register(this._imageRemoteRadioButton.onClicked(e => { + this._imageBrowseButton.style.display = 'none'; + this._imageUrlLabel.innerText = this.urlPlaceholder; + this._imageUrlInputBox.setPlaceHolder(this.urlPlaceholder); + })); + this._register(this._imageLocalRadioButton.onClicked(e => { + this._imageBrowseButton.style.display = 'block'; + this._imageUrlLabel.innerText = this.pathPlaceholder; + this._imageUrlInputBox.setPlaceHolder(this.pathPlaceholder); + })); + DOM.append(pathRow, inputContainer); + + let embedRow = DOM.$('.row'); + DOM.append(imageContentColumn, embedRow); + this._imageEmbedLabel = DOM.append(embedRow, DOM.$('.checkbox')); + this._imageEmbedCheckbox = new Checkbox( + this._imageEmbedLabel, + { + label: this.embedImageLabel, + checked: false, + onChange: (viaKeyboard) => { }, + ariaLabel: this.embedImageLabel + }); + DOM.append(embedRow, this._imageEmbedLabel); + } + + private buildInsertLinkCallout(container: HTMLElement): void { + let linkContentColumn = DOM.$('.column.insert-link'); + DOM.append(container, linkContentColumn); + + let linkTextRow = DOM.$('.row'); + DOM.append(linkContentColumn, linkTextRow); + + this._linkTextLabel = DOM.$('p'); + this._linkTextLabel.innerText = this.linkTextLabel; + DOM.append(linkTextRow, this._linkTextLabel); + + const linkTextInputContainer = DOM.$('.input-field'); + this._linkTextInputBox = new InputBox( + linkTextInputContainer, + this._contextViewService, + { + placeholder: this.linkTextPlaceholder, + ariaLabel: this.linkTextLabel + }); + DOM.append(linkTextRow, linkTextInputContainer); + + let linkAddressRow = DOM.$('.row'); + DOM.append(linkContentColumn, linkAddressRow); + this._linkAddressLabel = DOM.$('p'); + this._linkAddressLabel.innerText = this.linkAddressLabel; + DOM.append(linkAddressRow, this._linkAddressLabel); + + const linkAddressInputContainer = DOM.$('.input-field'); + this._linkUrlInputBox = new InputBox( + linkAddressInputContainer, + this._contextViewService, + { + placeholder: this.linkAddressPlaceholder, + ariaLabel: this.linkAddressLabel + }); + DOM.append(linkAddressRow, linkAddressInputContainer); + } + + private registerListeners(): void { + // Theme styler + if (this._calloutType === 'IMAGE') { + this._register(styler.attachInputBoxStyler(this._imageUrlInputBox, this._themeService)); + this._register(styler.attachCheckboxStyler(this._imageEmbedCheckbox, this._themeService)); + } + if (this._calloutType === 'LINK') { + this._register(styler.attachInputBoxStyler(this._linkTextInputBox, this._themeService)); + this._register(styler.attachInputBoxStyler(this._linkUrlInputBox, this._themeService)); + } + } + + protected layout(height?: number): void { + } + + public insert() { + this.hide(); + if (this._calloutType === 'IMAGE') { + this._selectionComplete.resolve({ + insertMarkup: ``, + imagePath: this._imageUrlInputBox.value, + embedImage: this._imageEmbedCheckbox.checked + }); + } + if (this._calloutType === 'LINK') { + this._selectionComplete.resolve({ + insertMarkup: `${strings.escape(this._linkTextInputBox.value)}`, + }); + } + this.dispose(); + } + + public cancel() { + this.hide(); + this._selectionComplete.resolve({ + insertMarkup: '', + imagePath: undefined, + embedImage: undefined + }); + this.dispose(); + } + + private async getUserHome(): Promise { + const userHomeUri = await this._pathService.userHome(); + return userHomeUri.path; + } + + private async handleBrowse(): Promise { + let options: IOpenDialogOptions = { + openLabel: undefined, + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + defaultUri: URI.file(await this.getUserHome()), + title: undefined + }; + let imageUri: URI[] = await this._fileDialogService.showOpenDialog(options); + if (imageUri.length > 0) { + return imageUri[0]; + } else { + return undefined; + } + } +} diff --git a/src/sql/workbench/browser/modal/media/browse-local.svg b/src/sql/workbench/browser/modal/media/browse-local.svg new file mode 100644 index 0000000000..d187a6af20 --- /dev/null +++ b/src/sql/workbench/browser/modal/media/browse-local.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/sql/workbench/browser/modal/media/modal.css b/src/sql/workbench/browser/modal/media/modal.css index 6137be9b9a..107c68e499 100644 --- a/src/sql/workbench/browser/modal/media/modal.css +++ b/src/sql/workbench/browser/modal/media/modal.css @@ -13,25 +13,131 @@ z-index: 500; } -.modal:not(.flyout-dialog) .modal-dialog { +.modal .btn-secondary { + border-style: solid; + border-width: 1px; +} + +.modal:not(.flyout-dialog):not(.callout-dialog) .modal-dialog { margin: auto; width: 640px; height: 480px; } +.modal.callout-dialog { + background-color: transparent; +} +.modal.callout-dialog .modal-dialog { + border-radius: 2px; + box-shadow: 0px 3px 8px rgba(var(--foreground)); + max-height: 300px; + position: absolute; +} + +.modal.callout-dialog .modal-content .insert-image .flex-container { + display: flex; +} +.modal.callout-dialog .modal-content .insert-image .flex-container > div { + flex: 1; +} +.modal.callout-dialog .modal-content p { + margin: 0; +} +.modal.callout-dialog .modal-content .button-icon { + cursor: pointer; + margin-left: 10px; +} +.modal.callout-dialog .modal-content .insert-image .monaco-inputbox { + min-width: 380px; +} +.modal.callout-dialog .modal-content .row { + margin-bottom: 16px; +} +.modal.callout-dialog .modal-content .radio-group input { + margin-right: 8px; +} +.modal.callout-dialog .modal-content .radio-group span { + margin-right: 15px; +} + +.hc-black .modal.callout-dialog .modal-dialog { + box-shadow: none; +} + +/* Correct the arrow appearance for HC theme */ +.callout-arrow:before { + border-width: 1px; + border-style: solid; + border-color: + transparent + transparent + var(--bodybackground) + var(--bodybackground); + box-shadow: -3px 3px 3px 0 rgba(var(--foreground)); + content: ''; + display: block; + height: 0; + position: absolute; + width: 0; +} +.callout-arrow.from-below:before { + border-width: 0.5em; + left: 2em; + top: -0.2em; + transform: rotate(135deg); +} +.callout-arrow.from-left:before { + background-color: var(--bodybackground); + height: 26px; + right: -13px; + top: 26px; + transform: rotate(-135deg); + width: 26px; +} + +.hc-black .callout-arrow:before { + background-color: var(--bodybackground); + border-color: + transparent + transparent + var(--border) + var(--border); + border-width: 0.1em; + box-shadow: none; + height: 0.8em; + width: 0.8em; +} +.hc-black .callout-arrow.from-below:before { + top: -0.4em; +} +.hc-black .callout-arrow.from-left:before { + height: 2em; + right: -1.2em; + width: 2em; +} + + .modal .modal-header { padding: 15px; } +.modal.callout-dialog .modal-header { + padding: 18px 24px 8px 24px; +} + .modal .modal-footer { padding: 15px; } +.modal.callout-dialog .modal-footer { + padding: 15px 24px 15px 24px; +} .modal .codicon.in-progress { width: 25px; height: 25px; } +/** FLYOUT **/ .modal.flyout-dialog .modal-dialog { margin: auto auto auto auto; height: 100%; @@ -83,6 +189,17 @@ overflow: hidden; } +.modal.callout-dialog .modal-body { + padding: 8px 24px; +} + +.modal.callout-dialog.compact .modal-header { + padding: 16px 24px 4px 24px; +} +.modal.callout-dialog.compact .modal-body { + padding: 4px 24px 16px 24px; +} + /* modl body content style(excluding dialogErrorMessage section) for angulr component dialog */ .angular-modal-body-content { overflow-x: hidden; @@ -114,25 +231,16 @@ padding-left: 4px; } -.vs-dark .modal.flyout-dialog .input { - background-color: #3C3C3C; -} - -.vs-dark .modal.flyout-dialog input:disabled { - background-color: #E1E1E1; - color: #3C3C3C; -} - .modal .select-box, .modal .monaco-select-box { width: 100%; height: 25px; - color: #6C6C6C; font-size: 11px; border: 1px solid transparent; } .modal .modal-footer { + border-top: 1px solid #E1E1E1; display: flex; } @@ -149,9 +257,11 @@ } .modal .footer-button a.monaco-button.monaco-text-button { - min-width: 100px; + border-radius: 2px; + height: 24px; padding-left: 20px; padding-right: 20px; + min-width: 80px; } .vs .monaco-text-button:focus { @@ -159,14 +269,13 @@ } .modal .footer-button { - margin-left: 5px; + margin-left: 8px; } .modal .right-footer .footer-button:last-of-type { margin-right: none; } - .modal.flyout-dialog .dialog-message { padding: 6px 10px 10px 10px; font-size: 13px; @@ -176,20 +285,20 @@ .vs .modal.flyout-dialog .dialog-message.error, .vs-dark .modal.flyout-dialog .dialog-message.error { - background-color:#B62E00 !important; - color:#FFFFFF !important; + background-color: #B62E00 !important; + color: #FFFFFF !important; } .vs .modal.flyout-dialog .dialog-message.warning, .vs-dark .modal.flyout-dialog .dialog-message.warning { - background-color:#F9E385 !important; - color:#4A4A4A !important; + background-color: #F9E385 !important; + color: #4A4A4A !important; } .vs .modal.flyout-dialog .dialog-message.info, .vs-dark .modal.flyout-dialog .dialog-message.info { - background-color:#096CC9 !important; - color:#FFFFFF !important; + background-color: #096CC9 !important; + color: #FFFFFF !important; } .modal.flyout-dialog .dialog-message-header { @@ -226,18 +335,18 @@ } .modal.flyout-dialog .dialog-message.info .dialog-message-button > a:focus, -.modal.flyout-dialog .dialog-message.error .dialog-message-button > a:focus{ +.modal.flyout-dialog .dialog-message.error .dialog-message-button > a:focus { outline-color: #FFFFFF; } -.modal.flyout-dialog .dialog-message.warning .dialog-message-button > a:focus{ +.modal.flyout-dialog .dialog-message.warning .dialog-message-button > a:focus { outline-color: #000000; } .modal.flyout-dialog .dialog-message-button > a { background-position-x: 2px !important; background-color: inherit !important; - color:inherit !important; + color: inherit !important; padding-left: 22px !important; background-size: 16px 16px !important; text-align: left !important; @@ -288,7 +397,7 @@ background: url('show_details.svg') center center no-repeat; } -.dialog-message.info .dialog-message-icon { +.dialog-message.info .dialog-message-icon { background: url('info_notification_inverse.svg') center center no-repeat; } @@ -303,3 +412,18 @@ .dialog-message.error .dialog-message-icon { background: url('error_notification_inverse.svg') center center no-repeat; } + +.codicon.masked-icon.browse-local { + display: inline-block; + height: 25px; + width: 25px; +} +.codicon.masked-icon.browse-local:before { + height: 25px; + width: 25px; + background-image: none; + -webkit-mask-image: url('browse-local.svg'); + mask-image: url('browse-local.svg'); + -webkit-mask-size: 100%; + mask-size: 100%; +} diff --git a/src/sql/workbench/browser/modal/modal.ts b/src/sql/workbench/browser/modal/modal.ts index 2789b2743b..5fe356ea7c 100644 --- a/src/sql/workbench/browser/modal/modal.ts +++ b/src/sql/workbench/browser/modal/modal.ts @@ -17,7 +17,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { Button } from 'sql/base/browser/ui/button/button'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { localize } from 'vs/nls'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { ILogService } from 'vs/platform/log/common/log'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; @@ -27,6 +26,10 @@ import { IThemable } from 'vs/base/common/styler'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { alert } from 'vs/base/browser/ui/aria/aria'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { editorWidgetForeground, editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { notebookToolbarLines } from 'sql/platform/theme/common/colorRegistry'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; export enum MessageLevel { Error = 0, @@ -56,9 +59,21 @@ export interface IModalDialogStyles { } export type DialogWidth = 'narrow' | 'medium' | 'wide' | number; +export type DialogStyle = 'normal' | 'flyout' | 'callout'; +export type DialogPosition = 'left' | 'below'; + +export interface IDialogProperties { + xPos: number, + yPos: number, + width: number, + height: number +} export interface IModalOptions { - isFlyout?: boolean; + dialogStyle?: DialogStyle; + dialogPosition?: DialogPosition; + positionX?: number; + positionY?: number; width?: DialogWidth; isAngular?: boolean; hasBackButton?: boolean; @@ -66,16 +81,25 @@ export interface IModalOptions { hasErrors?: boolean; hasSpinner?: boolean; spinnerTitle?: string; + renderHeader?: boolean; + renderFooter?: boolean; + dialogProperties?: IDialogProperties; } const defaultOptions: IModalOptions = { - isFlyout: true, + dialogStyle: 'flyout', + dialogPosition: undefined, + positionX: undefined, + positionY: undefined, width: 'narrow', isAngular: false, hasBackButton: false, hasTitleIcon: false, hasErrors: false, - hasSpinner: false + hasSpinner: false, + renderHeader: true, + renderFooter: true, + dialogProperties: undefined }; const tabbableElementsQuerySelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'; @@ -106,6 +130,7 @@ export abstract class Modal extends Disposable implements IThemable { private _dialogBorder?: Color; private _dialogHeaderAndFooterBackground?: Color; private _dialogBodyBackground?: Color; + private _footerBorderTopColor?: Color; private _modalDialog?: HTMLElement; private _modalContent?: HTMLElement; @@ -168,18 +193,31 @@ export abstract class Modal extends Disposable implements IThemable { /** * Build and render the modal, will call {@link Modal#renderBody} + * */ public render() { + let top: number; let builderClass = '.modal.fade'; - if (this._modalOptions.isFlyout) { - builderClass += '.flyout-dialog'; - } + builderClass += this._modalOptions.dialogStyle === 'flyout' ? '.flyout-dialog' + : this._modalOptions.dialogStyle === 'callout' ? '.callout-dialog' + : ''; this._bodyContainer = DOM.$(`${builderClass}`, { role: 'dialog', 'aria-label': this._title }); - const top = this.layoutService.offset?.top ?? 0; + + if (this._modalOptions.dialogStyle === 'callout') { + top = 0; + } else { + top = this.layoutService.offset?.top ?? 0; + } this._bodyContainer.style.top = `${top}px`; this._modalDialog = DOM.append(this._bodyContainer, DOM.$('.modal-dialog')); - this._modalContent = DOM.append(this._modalDialog, DOM.$('.modal-content')); + + if (this._modalOptions.dialogStyle === 'callout') { + let arrowClass = `.callout-arrow.from-${this._modalOptions.dialogPosition}`; + this._modalContent = DOM.append(this._modalDialog, DOM.$(`.modal-content${arrowClass}`)); + } else { + this._modalContent = DOM.append(this._modalDialog, DOM.$('.modal-content')); + } if (typeof this._modalOptions.width === 'number') { this._modalDialog.style.width = `${this._modalOptions.width}px`; @@ -187,23 +225,29 @@ export abstract class Modal extends Disposable implements IThemable { this._modalDialog.classList.add(`${this._modalOptions.width}-dialog`); } + if (this._modalOptions.dialogStyle === 'callout') { + this._register(DOM.addDisposableListener(this._bodyContainer, DOM.EventType.CLICK, (e) => this.handleClickOffModal(e))); + } + if (!isUndefinedOrNull(this._title)) { - this._modalHeaderSection = DOM.append(this._modalContent, DOM.$('.modal-header')); - if (this._modalOptions.hasBackButton) { - const container = DOM.append(this._modalHeaderSection, DOM.$('.modal-go-back')); - this._backButton = new Button(container, { secondary: true }); - this._backButton.icon = { - classNames: 'backButtonIcon' - }; - this._backButton.title = localize('modal.back', "Back"); - } + if (this._modalOptions.renderHeader || this._modalOptions.renderHeader === undefined) { + this._modalHeaderSection = DOM.append(this._modalContent, DOM.$('.modal-header')); + if (this._modalOptions.hasBackButton) { + const container = DOM.append(this._modalHeaderSection, DOM.$('.modal-go-back')); + this._backButton = new Button(container, { secondary: true }); + this._backButton.icon = { + classNames: 'backButtonIcon' + }; + this._backButton.title = localize('modal.back', "Back"); + } - if (this._modalOptions.hasTitleIcon) { - this._modalTitleIcon = DOM.append(this._modalHeaderSection, DOM.$('.modal-title-icon')); - } + if (this._modalOptions.hasTitleIcon) { + this._modalTitleIcon = DOM.append(this._modalHeaderSection, DOM.$('.modal-title-icon')); + } - this._modalTitle = DOM.append(this._modalHeaderSection, DOM.$('h1.modal-title')); - this._modalTitle.innerText = this._title; + this._modalTitle = DOM.append(this._modalHeaderSection, DOM.$('h1.modal-title')); + this._modalTitle.innerText = this._title; + } } if (!this._modalOptions.isAngular && this._modalOptions.hasErrors) { @@ -249,16 +293,17 @@ export abstract class Modal extends Disposable implements IThemable { this._modalBodySection = DOM.append(this._modalContent, DOM.$(`.${modalBodyClass}`)); this.renderBody(this._modalBodySection); - // This modal footer section refers to the footer of of the dialog - if (!this._modalOptions.isAngular) { - this._modalFooterSection = DOM.append(this._modalContent, DOM.$('.modal-footer')); - if (this._modalOptions.hasSpinner) { - this._spinnerElement = DOM.append(this._modalFooterSection, DOM.$('.codicon.in-progress')); - this._spinnerElement.setAttribute('title', this._modalOptions.spinnerTitle ?? ''); - DOM.hide(this._spinnerElement); + if (this._modalOptions.renderFooter !== false) { + if (!this._modalOptions.isAngular) { + this._modalFooterSection = DOM.append(this._modalContent, DOM.$('.modal-footer')); + if (this._modalOptions.hasSpinner) { + this._spinnerElement = DOM.append(this._modalFooterSection, DOM.$('.codicon.in-progress')); + this._spinnerElement.setAttribute('title', this._modalOptions.spinnerTitle ?? ''); + DOM.hide(this._spinnerElement); + } + this._leftFooter = DOM.append(this._modalFooterSection, DOM.$('.left-footer')); + this._rightFooter = DOM.append(this._modalFooterSection, DOM.$('.right-footer')); } - this._leftFooter = DOM.append(this._modalFooterSection, DOM.$('.left-footer')); - this._rightFooter = DOM.append(this._modalFooterSection, DOM.$('.right-footer')); } } @@ -275,6 +320,20 @@ export abstract class Modal extends Disposable implements IThemable { this.hide(); } + /** + * Used to close modal when a click occurs outside the modal. + * This is exclusive to the Callout. + * @param e The Callout modal click event + */ + private handleClickOffModal(e: MouseEvent): void { + const target = e.target as HTMLElement; + if (target.closest('.modal-content')) { + return; + } else { + this.hide(); + } + } + /** * Overridable to change behavior of enter key */ @@ -357,16 +416,56 @@ export abstract class Modal extends Disposable implements IThemable { } } + + /** + * Tasks to perform before dialog is shown + * Includes: positioning of dialog + */ + protected positionDialog(): void { + /** + * In the case of 'below', dialog will be positioned beneath the trigger and arrow aligned with trigger. + * In the case of 'left', dialog will be positioned left of the trigger and arrow aligned with trigger. + */ + if (this._modalOptions.dialogStyle === 'callout') { + let dialogWidth; + if (typeof this._modalOptions.width === 'number') { + dialogWidth = this._modalOptions.width; + } + + if (this._modalOptions.dialogPosition === 'below') { + if (this._modalOptions.dialogProperties) { + this._modalDialog.style.left = `${this._modalOptions.dialogProperties.xPos - this._modalOptions.dialogProperties.width}px`; + this._modalDialog.style.top = `${this._modalOptions.dialogProperties.yPos + (this._modalOptions.dialogProperties.height)}px`; + } else { + this._modalDialog.style.left = `${this._modalOptions.positionX}px`; + this._modalDialog.style.top = `${this._modalOptions.positionY}px`; + } + } + + if (this._modalOptions.dialogPosition === 'left') { + if (this._modalOptions.dialogProperties) { + this._modalDialog.style.left = `${this._modalOptions.positionX - (dialogWidth + this._modalOptions.dialogProperties.width)}px`; + this._modalDialog.style.top = `${this._modalOptions.positionY - this._modalOptions.dialogProperties.height * 2}px`; + } else { + this._modalDialog.style.left = `${this._modalOptions.positionX - (dialogWidth)}px`; + this._modalDialog.style.top = `${this._modalOptions.positionY}px`; + } + } + this._modalDialog.style.width = `${dialogWidth}px`; + } + } + /** * Shows the modal and attaches key listeners */ protected show() { + this.positionDialog(); this._focusedElementBeforeOpen = document.activeElement; this._modalShowingContext.get()!.push(this._staticKey); DOM.append(this.layoutService.container, this._bodyContainer!); this.setInitialFocusedElement(); - this.disposableStore.add(DOM.addDisposableListener(document, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + this.disposableStore.add(DOM.addDisposableListener(document, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { let context = this._modalShowingContext.get()!; if (context[context.length - 1] === this._staticKey) { let event = new StandardKeyboardEvent(e); @@ -423,6 +522,7 @@ export abstract class Modal extends Disposable implements IThemable { * 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 + * @param isSecondary Set the css class if true */ protected addFooterButton(label: string, onSelect: () => void, orientation: 'left' | 'right' = 'right', isSecondary: boolean = false): Button { let footerButton = DOM.$('.footer-button'); @@ -434,6 +534,7 @@ export abstract class Modal extends Disposable implements IThemable { } else { DOM.append(this._rightFooter!, footerButton); } + this._footerButtons.push(button); return button; } @@ -541,8 +642,8 @@ export abstract class Modal extends Disposable implements IThemable { /** * Return background color of header and footer */ - protected get headerAndFooterBackground(): string | null { - return this._dialogHeaderAndFooterBackground ? this._dialogHeaderAndFooterBackground.toString() : null; + protected get headerAndFooterBackground(): string | undefined { + return this._dialogHeaderAndFooterBackground ? this._dialogHeaderAndFooterBackground.toString() : undefined; } /** @@ -575,10 +676,15 @@ export abstract class Modal extends Disposable implements IThemable { * 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._dialogForeground = styles.dialogForeground ? styles.dialogForeground : this._themeService.getColorTheme().getColor(editorWidgetForeground); + this._dialogBorder = styles.dialogBorder ? styles.dialogBorder : this._themeService.getColorTheme().getColor(notebookToolbarLines); + if (this._modalOptions.dialogStyle === 'callout') { + this._dialogHeaderAndFooterBackground = styles.dialogBodyBackground ? styles.dialogBodyBackground : this._themeService.getColorTheme().getColor(SIDE_BAR_BACKGROUND); + } else { + this._dialogHeaderAndFooterBackground = styles.dialogHeaderAndFooterBackground ? styles.dialogHeaderAndFooterBackground : this._themeService.getColorTheme().getColor(SIDE_BAR_BACKGROUND); + } + this._dialogBodyBackground = styles.dialogBodyBackground ? styles.dialogBodyBackground : this._themeService.getColorTheme().getColor(editorBackground); + this._footerBorderTopColor = styles.footerBorderTopColor ? styles.footerBorderTopColor : this._themeService.getColorTheme().getColor(notebookToolbarLines); this.applyStyles(); } @@ -587,8 +693,10 @@ export abstract class Modal extends Disposable implements IThemable { const border = this._dialogBorder ? this._dialogBorder.toString() : ''; const headerAndFooterBackground = this._dialogHeaderAndFooterBackground ? this._dialogHeaderAndFooterBackground.toString() : ''; const bodyBackground = this._dialogBodyBackground ? this._dialogBodyBackground.toString() : ''; - const footerBorderTopWidth = border ? '1px' : ''; - const footerBorderTopStyle = border ? 'solid' : ''; + const calloutStyle: CSSStyleDeclaration = this._modalDialog.style; + const footerTopBorderColor = this._footerBorderTopColor ? this._footerBorderTopColor.toString() : ''; + + const foregroundRgb: Color = Color.Format.CSS.parseHex(foreground); if (this._closeButtonInHeader) { this._closeButtonInHeader.style.color = foreground; @@ -598,12 +706,25 @@ export abstract class Modal extends Disposable implements IThemable { this._modalDialog.style.borderWidth = border ? '1px' : ''; this._modalDialog.style.borderStyle = border ? 'solid' : ''; this._modalDialog.style.borderColor = border; + + calloutStyle.setProperty('--border', `${border}`); + calloutStyle.setProperty('--bodybackground', `${bodyBackground}`); + if (foregroundRgb) { + calloutStyle.setProperty('--foreground', ` + ${foregroundRgb.rgba.r}, + ${foregroundRgb.rgba.g}, + ${foregroundRgb.rgba.b}, + 0.08 + `); + } } if (this._modalHeaderSection) { this._modalHeaderSection.style.backgroundColor = headerAndFooterBackground; - this._modalHeaderSection.style.borderBottomWidth = border ? '1px' : ''; - this._modalHeaderSection.style.borderBottomStyle = border ? 'solid' : ''; + if (!(this._modalOptions.dialogStyle === 'callout')) { + this._modalHeaderSection.style.borderBottomWidth = border ? '1px' : ''; + this._modalHeaderSection.style.borderBottomStyle = border ? 'solid' : ''; + } this._modalHeaderSection.style.borderBottomColor = border; } @@ -620,9 +741,9 @@ export abstract class Modal extends Disposable implements IThemable { if (this._modalFooterSection) { this._modalFooterSection.style.backgroundColor = headerAndFooterBackground; - this._modalFooterSection.style.borderTopWidth = footerBorderTopWidth; - this._modalFooterSection.style.borderTopStyle = footerBorderTopStyle; - this._modalFooterSection.style.borderTopColor = border; + this._modalFooterSection.style.borderTopWidth = border ? '1px' : ''; + this._modalFooterSection.style.borderTopStyle = border ? 'solid' : ''; + this._modalFooterSection.style.borderTopColor = footerTopBorderColor; } } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component.ts index e2bd00a014..61885a0db8 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component.ts @@ -58,12 +58,12 @@ export class CellToolbarComponent { this._actionBar = new Taskbar(taskbar); this._actionBar.context = context; - let addCellsButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codeCellsPreview', "Add cell"), 'notebook-button masked-pseudo code'); + let addCellsButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codeCellsPreview', "Add cell"), 'masked-pseudo code'); - let addCodeCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codePreview', "Code cell"), 'notebook-button masked-pseudo code'); + let addCodeCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codePreview', "Code cell"), 'masked-pseudo code'); addCodeCellButton.cellType = CellTypes.Code; - let addTextCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddTextCell', localize('textPreview', "Text cell"), 'notebook-button masked-pseudo markdown'); + let addTextCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddTextCell', localize('textPreview', "Text cell"), 'masked-pseudo markdown'); addTextCellButton.cellType = CellTypes.Markdown; let moveCellDownButton = this.instantiationService.createInstance(MoveCellAction, 'notebook.MoveCellDown', 'masked-icon move-down', this.buttonMoveDown); diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.html b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.html index f182e9875b..8fa9ca6a30 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.html +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.html @@ -4,7 +4,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ --> -
+
diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts index 543c8a2f14..d67b127334 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { nb } from 'azdata'; -import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef, SimpleChange, OnChanges, HostListener, ViewChildren, QueryList } from '@angular/core'; +import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef, SimpleChange, OnChanges, ViewChildren, QueryList } from '@angular/core'; import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; @@ -12,6 +12,8 @@ import { Deferred } from 'sql/base/common/promise'; import { ICellEditorProvider } from 'sql/workbench/services/notebook/browser/notebookService'; import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/code.component'; import { OutputComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/output.component'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; export const CODE_SELECTOR: string = 'code-cell-component'; @@ -32,12 +34,6 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { this._activeCellId = value; } - @HostListener('document:keydown.escape', ['$event']) - handleKeyboardEvent() { - this.cellModel.active = false; - this._model.updateActiveCell(undefined); - } - private _model: NotebookModel; private _activeCellId: string; @@ -133,4 +129,12 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { public cellGuid(): string { return this.cellModel.cellGuid; } + + public onKey(e: KeyboardEvent) { + let event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Escape)) { + this.cellModel.active = false; + this._model.updateActiveCell(undefined); + } + } } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts index a3afe4f2dc..778ca22420 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts @@ -4,14 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./markdownToolbar'; import * as DOM from 'vs/base/browser/dom'; +import { Button, IButtonStyles } from 'sql/base/browser/ui/button/button'; import { Component, Input, Inject, ViewChild, ElementRef } from '@angular/core'; import { localize } from 'vs/nls'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { TransformMarkdownAction, MarkdownButtonType, ToggleViewAction } from 'sql/workbench/contrib/notebook/browser/markdownToolbarActions'; +import { TransformMarkdownAction, MarkdownTextTransformer, MarkdownButtonType, ToggleViewAction } from 'sql/workbench/contrib/notebook/browser/markdownToolbarActions'; +import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DropdownMenuActionViewItem } from 'sql/base/browser/ui/buttonMenu/buttonMenu'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { AngularDisposable } from 'sql/base/browser/lifecycle'; export const MARKDOWN_TOOLBAR_SELECTOR: string = 'markdown-toolbar-component'; @@ -19,9 +23,11 @@ export const MARKDOWN_TOOLBAR_SELECTOR: string = 'markdown-toolbar-component'; selector: MARKDOWN_TOOLBAR_SELECTOR, templateUrl: decodeURI(require.toUrl('./markdownToolbar.component.html')) }) -export class MarkdownToolbarComponent { +export class MarkdownToolbarComponent extends AngularDisposable { @ViewChild('mdtoolbar', { read: ElementRef }) private mdtoolbar: ElementRef; + public previewFeaturesEnabled: boolean = false; + public buttonBold = localize('buttonBold', "Bold"); public buttonItalic = localize('buttonItalic', "Italic"); public buttonUnderline = localize('buttonUnderline', "Underline"); @@ -44,6 +50,7 @@ export class MarkdownToolbarComponent { private _taskbarContent: Array; private _wysiwygTaskbarContent: Array; + private _previewModeTaskbarContent: Array; @Input() public cellModel: ICellModel; private _actionBar: Taskbar; @@ -52,25 +59,65 @@ export class MarkdownToolbarComponent { _toggleMarkdownViewAction: ToggleViewAction; constructor( + @Inject(INotebookService) private _notebookService: INotebookService, @Inject(IInstantiationService) private _instantiationService: IInstantiationService, - @Inject(IContextMenuService) private contextMenuService: IContextMenuService - ) { } + @Inject(IContextMenuService) private _contextMenuService: IContextMenuService, + @Inject(IConfigurationService) private _configurationService: IConfigurationService + ) { + super(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + this.previewFeaturesEnabled = this._configurationService.getValue('workbench.enablePreviewFeatures'); + })); + } ngOnInit() { this.initActionBar(); } private initActionBar() { + this.previewFeaturesEnabled = this._configurationService.getValue('workbench.enablePreviewFeatures'); + + let linkButton: TransformMarkdownAction; + let imageButton: TransformMarkdownAction; + let linkButtonContainer: HTMLElement; + let imageButtonContainer: HTMLElement; + + if (this.previewFeaturesEnabled) { + linkButtonContainer = DOM.$('li.action-item'); + linkButtonContainer.setAttribute('role', 'presentation'); + let linkButton = new Button(linkButtonContainer); + linkButton.element.setAttribute('class', 'action-label codicon insert-link masked-icon'); + let buttonStyle: IButtonStyles = { + buttonBackground: null + }; + linkButton.style(buttonStyle); + + this._register(DOM.addDisposableListener(linkButtonContainer, DOM.EventType.CLICK, e => { + this.onInsertButtonClick(e, MarkdownButtonType.LINK_PREVIEW); + })); + + imageButtonContainer = DOM.$('li.action-item'); + imageButtonContainer.setAttribute('role', 'presentation'); + let imageButton = new Button(imageButtonContainer); + imageButton.element.setAttribute('class', 'action-label codicon insert-image masked-icon'); + + imageButton.style(buttonStyle); + + this._register(DOM.addDisposableListener(imageButtonContainer, DOM.EventType.CLICK, e => { + this.onInsertButtonClick(e, MarkdownButtonType.IMAGE_PREVIEW); + })); + } else { + linkButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.linkText', '', 'insert-link masked-icon', this.buttonLink, this.cellModel, MarkdownButtonType.LINK); + imageButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.imageText', '', 'insert-image masked-icon', this.buttonImage, this.cellModel, MarkdownButtonType.IMAGE); + } + let boldButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.boldText', '', 'bold masked-icon', this.buttonBold, this.cellModel, MarkdownButtonType.BOLD); let italicButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.italicText', '', 'italic masked-icon', this.buttonItalic, this.cellModel, MarkdownButtonType.ITALIC); let underlineButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.underlineText', '', 'underline masked-icon', this.buttonUnderline, this.cellModel, MarkdownButtonType.UNDERLINE); let highlightButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.highlightText', '', 'highlight masked-icon', this.buttonHighlight, this.cellModel, MarkdownButtonType.HIGHLIGHT); let codeButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.codeText', '', 'code masked-icon', this.buttonCode, this.cellModel, MarkdownButtonType.CODE); - let linkButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.linkText', '', 'insert-link masked-icon', this.buttonLink, this.cellModel, MarkdownButtonType.LINK); let listButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.listText', '', 'list masked-icon', this.buttonList, this.cellModel, MarkdownButtonType.UNORDERED_LIST); let orderedListButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.orderedText', '', 'ordered-list masked-icon', this.buttonOrderedList, this.cellModel, MarkdownButtonType.ORDERED_LIST); - let imageButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.imageText', '', 'insert-image masked-icon', this.buttonImage, this.cellModel, MarkdownButtonType.IMAGE); - let headingDropdown = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading', '', 'heading', this.dropdownHeading, this.cellModel, null); let heading1 = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading1', this.optionHeading1, 'heading 1', this.optionHeading1, this.cellModel, MarkdownButtonType.HEADING1); let heading2 = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading2', this.optionHeading2, 'heading 2', this.optionHeading2, this.cellModel, MarkdownButtonType.HEADING2); @@ -90,11 +137,11 @@ export class MarkdownToolbarComponent { let dropdownMenuActionViewItem = new DropdownMenuActionViewItem( headingDropdown, [heading1, heading2, heading3, paragraph], - this.contextMenuService, + this._contextMenuService, undefined, this._actionBar.actionRunner, undefined, - 'notebook-button masked-pseudo-after dropdown-arrow', + 'masked-pseudo-after dropdown-arrow', this.optionParagraph, undefined ); @@ -129,20 +176,51 @@ export class MarkdownToolbarComponent { { action: this._toggleSplitViewAction }, { action: this._toggleMarkdownViewAction } ]; + + this._previewModeTaskbarContent = [ + { action: boldButton }, + { action: italicButton }, + { action: underlineButton }, + { action: highlightButton }, + { action: codeButton }, + { element: linkButtonContainer }, + { action: listButton }, + { action: orderedListButton }, + { element: imageButtonContainer }, + { element: buttonDropdownContainer }, + { action: this._toggleTextViewAction }, + { action: this._toggleSplitViewAction }, + { action: this._toggleMarkdownViewAction } + ]; + // Hide link and image buttons in WYSIWYG mode if (this.cellModel.showPreview && !this.cellModel.showMarkdown) { this._actionBar.setContent(this._wysiwygTaskbarContent); } else { - this._actionBar.setContent(this._taskbarContent); + if (this.previewFeaturesEnabled) { + this._actionBar.setContent(this._previewModeTaskbarContent); + } else { + this._actionBar.setContent(this._taskbarContent); + } } } + public onInsertButtonClick(event: MouseEvent, type: MarkdownButtonType): void { + let go = new MarkdownTextTransformer(this._notebookService, this.cellModel, this._instantiationService); + let trigger = event.target as HTMLElement; + go.transformText(type, trigger); + } + public hideLinkAndImageButtons() { this._actionBar.setContent(this._wysiwygTaskbarContent); } public showLinkAndImageButtons() { - this._actionBar.setContent(this._taskbarContent); + if (this.previewFeaturesEnabled) { + this._actionBar.setContent(this._previewModeTaskbarContent); + } else { + this._actionBar.setContent(this._taskbarContent); + } } public removeActiveClassFromModeActions() { diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.html b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.html index 2993db0106..923efba1c4 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.html +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.html @@ -4,7 +4,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ --> -
+
diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts index 2cd9652d05..15741d3fa7 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts @@ -29,6 +29,8 @@ import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/ import { NotebookRange, ICellEditorProvider, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { HTMLMarkdownConverter } from 'sql/workbench/contrib/notebook/browser/htmlMarkdownConverter'; import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; export const TEXT_SELECTOR: string = 'text-cell-component'; const USER_SELECT_CLASS = 'actionselect'; @@ -52,15 +54,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { this._activeCellId = value; } - @HostListener('document:keydown.escape', ['$event']) - handleKeyboardEvent() { - if (this.isEditMode) { - this.toggleEditMode(false); - } - this.cellModel.active = false; - this._model.updateActiveCell(undefined); - } - // Double click to edit text cell in notebook @HostListener('dblclick', ['$event']) onDblClick() { this.enableActiveCellEditOnDoubleClick(); @@ -457,6 +450,17 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { this.cellModel.active = true; this._model.updateActiveCell(this.cellModel); } + + public onKey(e: KeyboardEvent) { + let event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Escape)) { + if (this.isEditMode) { + this.toggleEditMode(false); + } + this.cellModel.active = false; + this._model.updateActiveCell(undefined); + } + } } function preventDefaultAndExecCommand(e: KeyboardEvent, commandId: string) { diff --git a/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts b/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts index d7c5877b68..9457b3f424 100644 --- a/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Action } from 'vs/base/common/actions'; +import { localize } from 'vs/nls'; import { INotebookEditor, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -15,7 +16,9 @@ import { Selection } from 'vs/editor/common/core/selection'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { MarkdownToolbarComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component'; - +import { CalloutDialog, CalloutType } from 'sql/workbench/browser/modal/calloutDialog'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { DialogWidth } from 'sql/workbench/api/common/sqlExtHostTypes'; export class TransformMarkdownAction extends Action { @@ -26,25 +29,20 @@ export class TransformMarkdownAction extends Action { tooltip: string, private _cellModel: ICellModel, private _type: MarkdownButtonType, - @INotebookService private _notebookService: INotebookService + @INotebookService private _notebookService: INotebookService, + @IInstantiationService private _instantiationService: IInstantiationService ) { super(id, label, cssClass); this._tooltip = tooltip; } - public run(context: any): Promise { - return new Promise((resolve, reject) => { - try { - if (!context?.cellModel?.showMarkdown && context?.cellModel?.showPreview) { - this.transformDocumentCommand(); - } else { - let markdownTextTransformer = new MarkdownTextTransformer(this._notebookService, this._cellModel); - markdownTextTransformer.transformText(this._type); - } - resolve(true); - } catch (e) { - reject(e); - } - }); + public async run(context: any): Promise { + if (!context?.cellModel?.showMarkdown && context?.cellModel?.showPreview) { + this.transformDocumentCommand(); + } else { + let markdownTextTransformer = new MarkdownTextTransformer(this._notebookService, this._cellModel, this._instantiationService); + await markdownTextTransformer.transformText(this._type); + } + return true; } private transformDocumentCommand() { @@ -105,12 +103,14 @@ export class TransformMarkdownAction extends Action { } break; case MarkdownButtonType.IMAGE: + case MarkdownButtonType.IMAGE_PREVIEW: // TODO break; case MarkdownButtonType.ITALIC: document.execCommand('italic'); break; case MarkdownButtonType.LINK: + case MarkdownButtonType.LINK_PREVIEW: document.execCommand('createLink', false, window.getSelection()?.focusNode?.textContent); break; case MarkdownButtonType.ORDERED_LIST: @@ -130,17 +130,21 @@ export class TransformMarkdownAction extends Action { } export class MarkdownTextTransformer { + private _callout: CalloutDialog; + private readonly insertLinkHeading = localize('callout.insertLinkHeading', "Insert link"); + private readonly insertImageHeading = localize('callout.insertImageHeading', "Insert image"); constructor( private _notebookService: INotebookService, private _cellModel: ICellModel, + private _instantiationService: IInstantiationService, private _notebookEditor?: INotebookEditor) { } public get notebookEditor(): INotebookEditor { return this._notebookEditor; } - public transformText(type: MarkdownButtonType): void { + public async transformText(type: MarkdownButtonType, triggerElement?: HTMLElement): Promise { let editorControl = this.getEditorControl(); if (editorControl) { let selections = editorControl.getSelections(); @@ -154,8 +158,15 @@ export class MarkdownTextTransformer { endLineNumber: selection.startLineNumber }; - let beginInsertedText = getStartTextToInsert(type); - let endInsertedText = getEndTextToInsert(type); + let beginInsertedText: string; + let endInsertedText: string; + + if (type === MarkdownButtonType.IMAGE_PREVIEW || type === MarkdownButtonType.LINK_PREVIEW) { + beginInsertedText = await this.createCallout(type, triggerElement); + } else { + beginInsertedText = getStartTextToInsert(type); + endInsertedText = getEndTextToInsert(type); + } let endRange: IRange = { startColumn: selection.endColumn, @@ -187,6 +198,37 @@ export class MarkdownTextTransformer { } } + /** + * Instantiate modal for use as callout when inserting Link or Image into markdown. + * @param calloutStyle Style of callout passed in to determine which callout is rendered. + * Returns markup created after user enters values and submits the callout. + */ + private async createCallout(type: MarkdownButtonType, triggerElement: HTMLElement): Promise { + const triggerPosX = triggerElement.getBoundingClientRect().left; + const triggerPosY = triggerElement.getBoundingClientRect().top; + const triggerHeight = triggerElement.offsetHeight; + const triggerWidth = triggerElement.offsetWidth; + /** + * Width value here reflects designs for Notebook callouts. + */ + const width: DialogWidth = 452; + + const calloutType: CalloutType = type === MarkdownButtonType.IMAGE_PREVIEW ? 'IMAGE' : 'LINK'; + + let title = type === MarkdownButtonType.IMAGE_PREVIEW ? this.insertImageHeading : this.insertLinkHeading; + + if (!this._callout) { + const dialogProperties = { xPos: triggerPosX, yPos: triggerPosY, width: triggerWidth, height: triggerHeight }; + this._callout = this._instantiationService.createInstance(CalloutDialog, calloutType, title, width, dialogProperties); + this._callout.render(); + } + let calloutOptions = await this._callout.open(); + calloutOptions.insertTitle = title; + calloutOptions.calloutType = calloutType; + + return calloutOptions.insertMarkup; + } + private getEditorControl(): CodeEditorWidget | undefined { if (!this._notebookEditor) { this._notebookEditor = this._notebookService.findNotebookEditor(this._cellModel?.notebookModel?.notebookUri); @@ -398,9 +440,11 @@ export enum MarkdownButtonType { CODE, HIGHLIGHT, LINK, + LINK_PREVIEW, UNORDERED_LIST, ORDERED_LIST, IMAGE, + IMAGE_PREVIEW, HEADING1, HEADING2, HEADING3, @@ -469,12 +513,14 @@ function getStartTextToInsert(type: MarkdownButtonType): string { case MarkdownButtonType.CODE: return '```\n'; case MarkdownButtonType.LINK: + case MarkdownButtonType.LINK_PREVIEW: return '['; case MarkdownButtonType.UNORDERED_LIST: return '- '; case MarkdownButtonType.ORDERED_LIST: return '1. '; case MarkdownButtonType.IMAGE: + case MarkdownButtonType.IMAGE_PREVIEW: return '!['; case MarkdownButtonType.HIGHLIGHT: return ''; @@ -504,7 +550,9 @@ function getEndTextToInsert(type: MarkdownButtonType): string { case MarkdownButtonType.CODE: return '\n```'; case MarkdownButtonType.LINK: + case MarkdownButtonType.LINK_PREVIEW: case MarkdownButtonType.IMAGE: + case MarkdownButtonType.IMAGE_PREVIEW: return ']()'; case MarkdownButtonType.HIGHLIGHT: return ''; @@ -552,8 +600,10 @@ function getColumnOffsetForSelection(type: MarkdownButtonType, nothingSelected: } switch (type) { case MarkdownButtonType.LINK: + case MarkdownButtonType.LINK_PREVIEW: return 2; case MarkdownButtonType.IMAGE: + case MarkdownButtonType.IMAGE_PREVIEW: return 2; // -1 is considered as having no explicit offset, so do not do anything with selection default: return -1; diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index 6c58acf8f1..8253f30d9c 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -370,15 +370,15 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe let spacerElement = document.createElement('li'); spacerElement.style.marginLeft = 'auto'; - let addCellsButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codeCellsPreview', "Add cell"), 'notebook-button masked-pseudo code'); + let addCellsButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codeCellsPreview', "Add cell"), 'masked-pseudo code'); - let addCodeCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codePreview', "Code cell"), 'notebook-button masked-pseudo code'); + let addCodeCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codePreview', "Code cell"), 'masked-pseudo code'); addCodeCellButton.cellType = CellTypes.Code; - let addTextCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddTextCell', localize('textPreview', "Text cell"), 'notebook-button masked-pseudo markdown'); + let addTextCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddTextCell', localize('textPreview', "Text cell"), 'masked-pseudo markdown'); addTextCellButton.cellType = CellTypes.Markdown; - this._runAllCellsAction = this.instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAllPreview', "Run all"), 'notebook-button masked-pseudo start-outline'); + this._runAllCellsAction = this.instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAllPreview', "Run all"), 'masked-pseudo start-outline'); let collapseCellsAction = this.instantiationService.createInstance(CollapseCellsAction, 'notebook.collapseCells', true); @@ -401,7 +401,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe undefined, this._actionBar.actionRunner, undefined, - 'codicon notebook-button masked-pseudo masked-pseudo-after add-new dropdown-arrow', + 'codicon masked-pseudo masked-pseudo-after add-new dropdown-arrow', localize('addCell', "Cell"), undefined ); @@ -431,13 +431,13 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe attachToDropdown.render(attachToContainer); attachSelectBoxStyler(attachToDropdown, this.themeService); - let addCodeCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('code', "Code"), 'notebook-button icon-add'); + let addCodeCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('code', "Code"), 'icon-add'); addCodeCellButton.cellType = CellTypes.Code; - let addTextCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddTextCell', localize('text', "Text"), 'notebook-button icon-add'); + let addTextCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddTextCell', localize('text', "Text"), 'icon-add'); addTextCellButton.cellType = CellTypes.Markdown; - this._runAllCellsAction = this.instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAll', "Run Cells"), 'notebook-button icon-run-cells'); + this._runAllCellsAction = this.instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAll', "Run Cells"), 'icon-run-cells'); let clearResultsButton = this.instantiationService.createInstance(ClearAllOutputsAction, 'notebook.ClearAllOutputs', false); diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.css b/src/sql/workbench/contrib/notebook/browser/notebook.css index 32f9f354c8..6161bd268c 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.css +++ b/src/sql/workbench/contrib/notebook/browser/notebook.css @@ -37,28 +37,29 @@ min-height: 21px; } -.notebookEditor .editor-toolbar .actions-container .action-item .notebook-button.masked-pseudo { +.notebookEditor .editor-toolbar .actions-container .action-item .codicon.masked-pseudo { padding-right: 18px; } -.notebookEditor .editor-toolbar .actions-container .action-item .notebook-button { + +.notebookEditor .editor-toolbar .actions-container .action-item .codicon { display: inline-block; text-align: center; cursor: pointer; - padding: 0 18px; + padding: 0 0 0 18px; background-size: 13px; font-size: 13px; height: 21px; } -.notebookEditor .editor-toolbar .actions-container .action-item .notebook-button.masked-icon { +.notebookEditor .editor-toolbar .actions-container .action-item .codicon.masked-icon { padding: 0; width: 28px; } -.notebookEditor .in-preview .actions-container .action-item .notebook-button, -.notebookEditor .in-preview .actions-container .action-item .notebook-button:before { +.notebookEditor .in-preview .actions-container .action-item .codicon, +.notebookEditor .in-preview .actions-container .action-item .codicon:before { display: flex; height: 100%; - padding-right: 0px; + padding-right: 0; background-size: contain; } @@ -66,14 +67,14 @@ .in-preview .actions-container .action-item - .notebook-button.masked-pseudo { + .codicon.masked-pseudo { padding-left: 30px; } .notebookEditor .in-preview .actions-container .action-item - .notebook-button.masked-icon:before { + .codicon.masked-icon:before { margin-right: 0; padding-left: 18px; width: 16px; @@ -85,11 +86,11 @@ .in-preview .actions-container .action-item:last-child - .notebook-button { + .codicon { margin-right: 0; } -.notebookEditor .in-preview .actions-container .action-item:last-child .notebook-button.fixed-width { +.notebookEditor .in-preview .actions-container .action-item:last-child .codicon.fixed-width { background-size: contain; margin-left: 8px; padding: 0 0 0 18px; @@ -109,71 +110,71 @@ } /* non-preview */ -.notebookEditor :not(.in-preview) .notebook-button.icon-add { +.notebookEditor :not(.in-preview) .codicon.icon-add { background-image: url("./media/light/add.svg"); } -.vs-dark .notebookEditor :not(.in-preview) .notebook-button.icon-add, -.hc-black .notebookEditor :not(.in-preview) .notebook-button.icon-add { +.vs-dark .notebookEditor :not(.in-preview) .codicon.icon-add, +.hc-black .notebookEditor :not(.in-preview) .codicon.icon-add { background-image: url("./media/dark/add_inverse.svg"); } -.notebookEditor :not(.in-preview) .notebook-button.icon-run-cells { +.notebookEditor :not(.in-preview) .codicon.icon-run-cells { background-image: url("./media/light/run_cells.svg"); } -.vs-dark .notebookEditor :not(.in-preview) .notebook-button.icon-run-cells, -.hc-black .notebookEditor :not(.in-preview) .notebook-button.icon-run-cells { +.vs-dark .notebookEditor :not(.in-preview) .codicon.icon-run-cells, +.hc-black .notebookEditor :not(.in-preview) .codicon.icon-run-cells { background-image: url("./media/dark/run_cells_inverse.svg"); } -.notebookEditor :not(.in-preview) .notebook-button.icon-trusted { +.notebookEditor :not(.in-preview) .codicon.icon-trusted { background-image: url("./media/light/trusted.svg"); } -.vs-dark .notebookEditor :not(.in-preview) .notebook-button.icon-trusted, -.hc-black .notebookEditor :not(.in-preview) .notebook-button.icon-trusted { +.vs-dark .notebookEditor :not(.in-preview) .codicon.icon-trusted, +.hc-black .notebookEditor :not(.in-preview) .codicon.icon-trusted { background-image: url("./media/dark/trusted_inverse.svg"); } -.notebookEditor :not(.in-preview) .notebook-button.icon-notTrusted { +.notebookEditor :not(.in-preview) .codicon.icon-notTrusted { background-image: url("./media/light/nottrusted.svg"); } -.vs-dark .notebookEditor :not(.in-preview) .notebook-button.icon-notTrusted, -.hc-black .notebookEditor :not(.in-preview) .notebook-button.icon-notTrusted { +.vs-dark .notebookEditor :not(.in-preview) .codicon.icon-notTrusted, +.hc-black .notebookEditor :not(.in-preview) .codicon.icon-notTrusted { background-image: url("./media/dark/nottrusted_inverse.svg"); } -.notebookEditor :not(.in-preview) .notebook-button.icon-show-cells { +.notebookEditor :not(.in-preview) .codicon.icon-show-cells { background-image: url("./media/light/show_code.svg"); } -.vs-dark .notebookEditor :not(.in-preview) .notebook-button.icon-show-cells, -.hc-black .notebookEditor :not(.in-preview) .notebook-button.icon-show-cells { +.vs-dark .notebookEditor :not(.in-preview) .codicon.icon-show-cells, +.hc-black .notebookEditor :not(.in-preview) .codicon.icon-show-cells { background-image: url("./media/dark/show_code_inverse.svg"); } -.notebookEditor :not(.in-preview) .notebook-button.icon-hide-cells { +.notebookEditor :not(.in-preview) .codicon.icon-hide-cells { background-image: url("./media/light/hide_code.svg"); } -.vs-dark .notebookEditor :not(.in-preview) .notebook-button.icon-hide-cells, -.hc-black .notebookEditor :not(.in-preview) .notebook-button.icon-hide-cells { +.vs-dark .notebookEditor :not(.in-preview) .codicon.icon-hide-cells, +.hc-black .notebookEditor :not(.in-preview) .codicon.icon-hide-cells { background-image: url("./media/dark/hide_code_inverse.svg"); } -.notebookEditor :not(.in-preview) .notebook-button.icon-clear-results { +.notebookEditor :not(.in-preview) .codicon.icon-clear-results { background-image: url("./media/light/clear_results.svg"); } -.vs-dark .notebookEditor :not(.in-preview) .notebook-button.icon-clear-results, -.hc-black .notebookEditor :not(.in-preview) .notebook-button.icon-clear-results { +.vs-dark .notebookEditor :not(.in-preview) .codicon.icon-clear-results, +.hc-black .notebookEditor :not(.in-preview) .codicon.icon-clear-results { background-image: url("./media/dark/clear_results_inverse.svg"); } -.notebookEditor .in-preview .notebook-button.masked-icon, -.vs-dark .notebookEditor .in-preview .notebook-button.icon-clear-results, -.hc-black .notebookEditor .in-preview .notebook-button.icon-clear-results { +.notebookEditor .in-preview .codicon.masked-icon, +.vs-dark .notebookEditor .in-preview .codicon.icon-clear-results, +.hc-black .notebookEditor .in-preview .codicon.icon-clear-results { background-image: none; } /* non-preview */ diff --git a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts index 2021ff18e5..c731a2219e 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts @@ -101,7 +101,7 @@ export abstract class TooltipFromLabelAction extends Action { // Action to clear outputs of all code cells. export class ClearAllOutputsAction extends TooltipFromLabelAction { private static readonly label = localize('clearResults', "Clear Results"); - private static readonly baseClass = 'notebook-button'; + private static readonly baseClass = 'codicon'; private static readonly iconClass = 'icon-clear-results'; private static readonly maskedIconClass = 'masked-icon'; @@ -170,7 +170,7 @@ export class TrustedAction extends ToggleableAction { // Constants private static readonly trustedLabel = localize('trustLabel', "Trusted"); private static readonly notTrustedLabel = localize('untrustLabel', "Not Trusted"); - private static readonly baseClass = 'notebook-button'; + private static readonly baseClass = 'codicon'; private static readonly previewTrustedCssClass = 'icon-shield'; private static readonly trustedCssClass = 'icon-trusted'; private static readonly previewNotTrustedCssClass = 'icon-shield-x'; @@ -232,7 +232,7 @@ export class RunAllCellsAction extends Action { export class CollapseCellsAction extends ToggleableAction { private static readonly collapseCells = localize('collapseAllCells', "Collapse Cells"); private static readonly expandCells = localize('expandAllCells', "Expand Cells"); - private static readonly baseClass = 'notebook-button'; + private static readonly baseClass = 'codicon'; private static readonly previewCollapseCssClass = 'icon-collapse-cells'; private static readonly collapseCssClass = 'icon-hide-cells'; private static readonly previewExpandCssClass = 'icon-expand-cells'; diff --git a/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts b/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts index 607210c244..aacb31a2bf 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts @@ -150,8 +150,8 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf //Notebook toolbar masked icons const notebookToolbarIconColor = theme.getColor(notebookToolbarIcon); if (notebookToolbarIconColor) { - collector.addRule(`.notebookEditor .notebook-button.masked-icon:before { background-color: ${notebookToolbarIconColor};}`); - collector.addRule(`.notebookEditor .notebook-button.masked-pseudo:before { background-color: ${notebookToolbarIconColor};}`); + collector.addRule(`.masked-icon:before { background-color: ${notebookToolbarIconColor};}`); + collector.addRule(`.masked-pseudo:before { background-color: ${notebookToolbarIconColor};}`); } const notebookToolbarLinesColor = theme.getColor(notebookToolbarLines); if (notebookToolbarLinesColor) { @@ -164,7 +164,7 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf } const buttonMenuArrowColor = theme.getColor(buttonMenuArrow); if (buttonMenuArrowColor) { - collector.addRule(`.notebookEditor .notebook-button.masked-pseudo-after:after { background-color: ${buttonMenuArrowColor};}`); + collector.addRule(`.notebookEditor .masked-pseudo-after:after { background-color: ${buttonMenuArrowColor};}`); } // Active cell border, cell toolbar border, cell toolbar icons, view toggle active button bottom border diff --git a/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts b/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts index 57ea53839d..5c51b682ab 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts @@ -73,7 +73,7 @@ suite('MarkdownTextTransformer', () => { cellModel = new CellModel(undefined, undefined, mockNotebookService.object); notebookEditor = new NotebookEditorStub({ cellGuid: cellModel.cellGuid, instantiationService: instantiationService }); - markdownTextTransformer = new MarkdownTextTransformer(mockNotebookService.object, cellModel, notebookEditor); + markdownTextTransformer = new MarkdownTextTransformer(mockNotebookService.object, cellModel, instantiationService, notebookEditor); mockNotebookService.setup(s => s.findNotebookEditor(TypeMoq.It.isAny())).returns(() => notebookEditor); let editor = notebookEditor.cellEditors[0].getEditor(); @@ -91,87 +91,87 @@ suite('MarkdownTextTransformer', () => { assert(!isUndefinedOrNull(widget.getModel()), 'Text model is undefined'); }); - test('Transform text with no previous selection', () => { - testWithNoSelection(MarkdownButtonType.BOLD, '****', true); - testWithNoSelection(MarkdownButtonType.BOLD, ''); - testWithNoSelection(MarkdownButtonType.ITALIC, '__', true); - testWithNoSelection(MarkdownButtonType.ITALIC, ''); - testWithNoSelection(MarkdownButtonType.CODE, '```\n\n```', true); - testWithNoSelection(MarkdownButtonType.CODE, ''); - testWithNoSelection(MarkdownButtonType.HIGHLIGHT, '', true); - testWithNoSelection(MarkdownButtonType.HIGHLIGHT, ''); - testWithNoSelection(MarkdownButtonType.LINK, '[]()', true); - testWithNoSelection(MarkdownButtonType.LINK, ''); - testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, '- ', true); - testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, ''); - testWithNoSelection(MarkdownButtonType.ORDERED_LIST, '1. ', true); - testWithNoSelection(MarkdownButtonType.ORDERED_LIST, ''); - testWithNoSelection(MarkdownButtonType.IMAGE, '![]()', true); - testWithNoSelection(MarkdownButtonType.IMAGE, ''); - testWithNoSelection(MarkdownButtonType.HEADING1, '# ', true); - testWithNoSelection(MarkdownButtonType.HEADING1, ''); - testWithNoSelection(MarkdownButtonType.HEADING2, '## ', true); - testWithNoSelection(MarkdownButtonType.HEADING2, ''); - testWithNoSelection(MarkdownButtonType.HEADING3, '### ', true); - testWithNoSelection(MarkdownButtonType.HEADING3, ''); + test('Transform text with no previous selection', async () => { + await testWithNoSelection(MarkdownButtonType.BOLD, '****', true); + await testWithNoSelection(MarkdownButtonType.BOLD, ''); + await testWithNoSelection(MarkdownButtonType.ITALIC, '__', true); + await testWithNoSelection(MarkdownButtonType.ITALIC, ''); + await testWithNoSelection(MarkdownButtonType.CODE, '```\n\n```', true); + await testWithNoSelection(MarkdownButtonType.CODE, ''); + await testWithNoSelection(MarkdownButtonType.HIGHLIGHT, '', true); + await testWithNoSelection(MarkdownButtonType.HIGHLIGHT, ''); + await testWithNoSelection(MarkdownButtonType.LINK, '[]()', true); + await testWithNoSelection(MarkdownButtonType.LINK, ''); + await testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, '- ', true); + await testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, ''); + await testWithNoSelection(MarkdownButtonType.ORDERED_LIST, '1. ', true); + await testWithNoSelection(MarkdownButtonType.ORDERED_LIST, ''); + await testWithNoSelection(MarkdownButtonType.IMAGE, '![]()', true); + await testWithNoSelection(MarkdownButtonType.IMAGE, ''); + await testWithNoSelection(MarkdownButtonType.HEADING1, '# ', true); + await testWithNoSelection(MarkdownButtonType.HEADING1, ''); + await testWithNoSelection(MarkdownButtonType.HEADING2, '## ', true); + await testWithNoSelection(MarkdownButtonType.HEADING2, ''); + await testWithNoSelection(MarkdownButtonType.HEADING3, '### ', true); + await testWithNoSelection(MarkdownButtonType.HEADING3, ''); }); - test('Transform text with one word selected', () => { - testWithSingleWordSelected(MarkdownButtonType.CODE, '```\nWORD\n```'); + test('Transform text with one word selected', async () => { + await testWithSingleWordSelected(MarkdownButtonType.CODE, '```\nWORD\n```'); }); - test('Transform text with multiple words selected', () => { - testWithMultipleWordsSelected(MarkdownButtonType.BOLD, '**Multi Words**'); - testWithMultipleWordsSelected(MarkdownButtonType.ITALIC, '_Multi Words_'); - testWithMultipleWordsSelected(MarkdownButtonType.CODE, '```\nMulti Words\n```'); - testWithMultipleWordsSelected(MarkdownButtonType.HIGHLIGHT, 'Multi Words'); - testWithMultipleWordsSelected(MarkdownButtonType.LINK, '[Multi Words]()'); - testWithMultipleWordsSelected(MarkdownButtonType.UNORDERED_LIST, '- Multi Words'); - testWithMultipleWordsSelected(MarkdownButtonType.ORDERED_LIST, '1. Multi Words'); - testWithMultipleWordsSelected(MarkdownButtonType.IMAGE, '![Multi Words]()'); + test('Transform text with multiple words selected', async () => { + await testWithMultipleWordsSelected(MarkdownButtonType.BOLD, '**Multi Words**'); + await testWithMultipleWordsSelected(MarkdownButtonType.ITALIC, '_Multi Words_'); + await testWithMultipleWordsSelected(MarkdownButtonType.CODE, '```\nMulti Words\n```'); + await testWithMultipleWordsSelected(MarkdownButtonType.HIGHLIGHT, 'Multi Words'); + await testWithMultipleWordsSelected(MarkdownButtonType.LINK, '[Multi Words]()'); + await testWithMultipleWordsSelected(MarkdownButtonType.UNORDERED_LIST, '- Multi Words'); + await testWithMultipleWordsSelected(MarkdownButtonType.ORDERED_LIST, '1. Multi Words'); + await testWithMultipleWordsSelected(MarkdownButtonType.IMAGE, '![Multi Words]()'); }); - test('Transform text with multiple lines selected', () => { - testWithMultipleLinesSelected(MarkdownButtonType.BOLD, '**Multi\nLines\nSelected**'); - testWithMultipleLinesSelected(MarkdownButtonType.ITALIC, '_Multi\nLines\nSelected_'); - testWithMultipleLinesSelected(MarkdownButtonType.CODE, '```\nMulti\nLines\nSelected\n```'); - testWithMultipleLinesSelected(MarkdownButtonType.HIGHLIGHT, 'Multi\nLines\nSelected'); - testWithMultipleLinesSelected(MarkdownButtonType.LINK, '[Multi\nLines\nSelected]()'); - testWithMultipleLinesSelected(MarkdownButtonType.UNORDERED_LIST, '- Multi\n- Lines\n- Selected'); - testWithMultipleLinesSelected(MarkdownButtonType.ORDERED_LIST, '1. Multi\n1. Lines\n1. Selected'); - testWithMultipleLinesSelected(MarkdownButtonType.IMAGE, '![Multi\nLines\nSelected]()'); - testWithMultipleLinesSelected(MarkdownButtonType.HEADING1, '# Multi\n# Lines\n# Selected'); - testWithMultipleLinesSelected(MarkdownButtonType.HEADING2, '## Multi\n## Lines\n## Selected'); - testWithMultipleLinesSelected(MarkdownButtonType.HEADING3, '### Multi\n### Lines\n### Selected'); + test('Transform text with multiple lines selected', async () => { + await testWithMultipleLinesSelected(MarkdownButtonType.BOLD, '**Multi\nLines\nSelected**'); + await testWithMultipleLinesSelected(MarkdownButtonType.ITALIC, '_Multi\nLines\nSelected_'); + await testWithMultipleLinesSelected(MarkdownButtonType.CODE, '```\nMulti\nLines\nSelected\n```'); + await testWithMultipleLinesSelected(MarkdownButtonType.HIGHLIGHT, 'Multi\nLines\nSelected'); + await testWithMultipleLinesSelected(MarkdownButtonType.LINK, '[Multi\nLines\nSelected]()'); + await testWithMultipleLinesSelected(MarkdownButtonType.UNORDERED_LIST, '- Multi\n- Lines\n- Selected'); + await testWithMultipleLinesSelected(MarkdownButtonType.ORDERED_LIST, '1. Multi\n1. Lines\n1. Selected'); + await testWithMultipleLinesSelected(MarkdownButtonType.IMAGE, '![Multi\nLines\nSelected]()'); + await testWithMultipleLinesSelected(MarkdownButtonType.HEADING1, '# Multi\n# Lines\n# Selected'); + await testWithMultipleLinesSelected(MarkdownButtonType.HEADING2, '## Multi\n## Lines\n## Selected'); + await testWithMultipleLinesSelected(MarkdownButtonType.HEADING3, '### Multi\n### Lines\n### Selected'); }); - test('Ensure notebook editor returns expected object', () => { + test('Ensure notebook editor returns expected object', async () => { assert.deepEqual(notebookEditor, markdownTextTransformer.notebookEditor, 'Notebook editor does not match expected value'); // Set markdown text transformer to not have a notebook editor passed in - markdownTextTransformer = new MarkdownTextTransformer(mockNotebookService.object, cellModel); + markdownTextTransformer = new MarkdownTextTransformer(mockNotebookService.object, cellModel, instantiationService); assert.equal(markdownTextTransformer.notebookEditor, undefined, 'No notebook editor should be returned'); // Even after text is attempted to be transformed, there should be no editor, and therefore nothing on the text model - markdownTextTransformer.transformText(MarkdownButtonType.BOLD); + await markdownTextTransformer.transformText(MarkdownButtonType.BOLD); assert.equal(markdownTextTransformer.notebookEditor, undefined, 'Notebook model does not have a valid uri, so no editor should be returned'); assert.equal(textModel.getValue(), '', 'No text should exist on the textModel'); }); - function testWithNoSelection(type: MarkdownButtonType, expectedValue: string, setValue = false): void { + async function testWithNoSelection(type: MarkdownButtonType, expectedValue: string, setValue = false): Promise { if (setValue) { textModel.setValue(''); } - markdownTextTransformer.transformText(type); + await markdownTextTransformer.transformText(type); assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with no selection failed (setValue ${setValue})`); } - function testWithSingleWordSelected(type: MarkdownButtonType, expectedValue: string): void { + async function testWithSingleWordSelected(type: MarkdownButtonType, expectedValue: string): Promise { let value = 'WORD'; textModel.setValue(value); // Test transformation (adding text) widget.setSelection({ startColumn: 1, startLineNumber: 1, endColumn: value.length + 1, endLineNumber: 1 }); assert.equal(textModel.getValueInRange(widget.getSelection()), value, 'Expected selection is not found'); - markdownTextTransformer.transformText(type); + await markdownTextTransformer.transformText(type); const textModelValue = textModel.getValue(); assert.equal(textModelValue, expectedValue, `${MarkdownButtonType[type]} with single word selection failed`); @@ -179,32 +179,32 @@ suite('MarkdownTextTransformer', () => { const valueRange = getValueRange(textModel, value); assert.notEqual(valueRange, undefined, 'Could not find value in model after transformation'); widget.setSelection(valueRange); - markdownTextTransformer.transformText(type); + await markdownTextTransformer.transformText(type); assert.equal(textModel.getValue(), value, `Undo operation for ${MarkdownButtonType[type]} with single word selection failed`); } - function testWithMultipleWordsSelected(type: MarkdownButtonType, expectedValue: string): void { + async function testWithMultipleWordsSelected(type: MarkdownButtonType, expectedValue: string): Promise { let value = 'Multi Words'; textModel.setValue(value); widget.setSelection({ startColumn: 1, startLineNumber: 1, endColumn: 12, endLineNumber: 1 }); assert.equal(textModel.getValueInRange(widget.getSelection()), value, 'Expected multi-word selection is not found'); - markdownTextTransformer.transformText(type); + await markdownTextTransformer.transformText(type); assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with multiple word selection failed`); // Test undo (removing text) const valueRange = getValueRange(textModel, value); assert.notEqual(valueRange, undefined, 'Could not find value in model after transformation'); widget.setSelection(valueRange); - markdownTextTransformer.transformText(type); + await markdownTextTransformer.transformText(type); assert.equal(textModel.getValue(), value, `Undo operation for ${MarkdownButtonType[type]} with multiple word selection failed`); } - function testWithMultipleLinesSelected(type: MarkdownButtonType, expectedValue: string): void { + async function testWithMultipleLinesSelected(type: MarkdownButtonType, expectedValue: string): Promise { let value = 'Multi\nLines\nSelected'; textModel.setValue(value); widget.setSelection({ startColumn: 1, startLineNumber: 1, endColumn: 9, endLineNumber: 3 }); assert.equal(textModel.getValueInRange(widget.getSelection()), value, 'Expected multi-line selection is not found'); - markdownTextTransformer.transformText(type); + await markdownTextTransformer.transformText(type); assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with multiple line selection failed`); // Test undo (removing text) @@ -213,7 +213,7 @@ suite('MarkdownTextTransformer', () => { valueRange = new Range(valueRange.startLineNumber, valueRange.startColumn, valueRange.endLineNumber + 2, 9); assert.notEqual(valueRange, undefined, 'Could not find value in model after transformation'); widget.setSelection(valueRange); - markdownTextTransformer.transformText(type); + await markdownTextTransformer.transformText(type); assert.equal(textModel.getValue(), value, `Undo operation for ${MarkdownButtonType[type]} with multiple line selection failed`); } }); diff --git a/src/sql/workbench/contrib/webview/browser/webViewDialog.ts b/src/sql/workbench/contrib/webview/browser/webViewDialog.ts index 34cd4a45c6..9c80dfb1b6 100644 --- a/src/sql/workbench/contrib/webview/browser/webViewDialog.ts +++ b/src/sql/workbench/contrib/webview/browser/webViewDialog.ts @@ -49,7 +49,7 @@ export class WebViewDialog extends Modal { @IWebviewService private readonly webviewService: IWebviewService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService ) { - super('', TelemetryKeys.WebView, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); + super('', TelemetryKeys.WebView, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { dialogStyle: 'normal', hasTitleIcon: true }); this._okLabel = localize('webViewDialog.ok', "OK"); this._closeLabel = localize('webViewDialog.close', "Close"); } diff --git a/src/sql/workbench/services/accountManagement/browser/autoOAuthDialog.ts b/src/sql/workbench/services/accountManagement/browser/autoOAuthDialog.ts index 027b7439ea..7705b24365 100644 --- a/src/sql/workbench/services/accountManagement/browser/autoOAuthDialog.ts +++ b/src/sql/workbench/services/accountManagement/browser/autoOAuthDialog.ts @@ -64,7 +64,7 @@ export class AutoOAuthDialog extends Modal { textResourcePropertiesService, contextKeyService, { - isFlyout: true, + dialogStyle: 'flyout', hasBackButton: true, hasSpinner: true } diff --git a/src/sql/workbench/services/dialog/browser/customDialogService.ts b/src/sql/workbench/services/dialog/browser/customDialogService.ts index c473fd8090..7840a6a683 100644 --- a/src/sql/workbench/services/dialog/browser/customDialogService.ts +++ b/src/sql/workbench/services/dialog/browser/customDialogService.ts @@ -20,6 +20,12 @@ export class CustomDialogService { public showDialog(dialog: Dialog, dialogName?: string, options?: IModalOptions): void { let name = dialogName ? dialogName : 'CustomDialog'; + + if (options && (options.dialogStyle === 'callout')) { + options.positionX = document.activeElement.getBoundingClientRect().left; + options.positionY = document.activeElement.getBoundingClientRect().top; + options.renderFooter = false; + } let dialogModal = this._instantiationService.createInstance(DialogModal, dialog, name, options || DefaultDialogOptions); this._dialogModals.set(dialog, dialogModal); dialogModal.render(); diff --git a/src/sql/workbench/services/dialog/browser/dialogModal.ts b/src/sql/workbench/services/dialog/browser/dialogModal.ts index 0275e7b231..cd05b49073 100644 --- a/src/sql/workbench/services/dialog/browser/dialogModal.ts +++ b/src/sql/workbench/services/dialog/browser/dialogModal.ts @@ -58,25 +58,31 @@ export class DialogModal extends Modal { super.render(); attachModalDialogStyler(this, this._themeService); - if (this.backButton) { + if (this._modalOptions.renderFooter !== false) { + this._modalOptions.renderFooter = true; + } + + if (this._modalOptions.renderFooter && this.backButton) { this.backButton.onDidClick(() => this.cancel()); attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND }); } - if (this._dialog.customButtons) { + if (this._modalOptions.renderFooter && this._dialog.customButtons) { this._dialog.customButtons.forEach(button => { let buttonElement = this.addDialogButton(button); this.updateButtonElement(buttonElement, button); }); } - this._doneButton = this.addDialogButton(this._dialog.okButton, () => this.done(), false, true); - this._dialog.okButton.registerClickEvent(this._onDone.event); - this._dialog.onValidityChanged(valid => { - this._doneButton.enabled = valid && this._dialog.okButton.enabled; - }); - this.addDialogButton(this._dialog.cancelButton, () => this.cancel(), false); - this._dialog.cancelButton.registerClickEvent(this._onCancel.event); + if (this._modalOptions.renderFooter) { + this._doneButton = this.addDialogButton(this._dialog.okButton, () => this.done(), false, true); + this._dialog.okButton.registerClickEvent(this._onDone.event); + this._dialog.onValidityChanged(valid => { + this._doneButton.enabled = valid && this._dialog.okButton.enabled; + }); + this.addDialogButton(this._dialog.cancelButton, () => this.cancel(), false); + this._dialog.cancelButton.registerClickEvent(this._onCancel.event); + } let messageChangeHandler = (message: DialogMessage) => { if (message && message.text) { diff --git a/src/sql/workbench/services/dialog/common/dialogTypes.ts b/src/sql/workbench/services/dialog/common/dialogTypes.ts index cc58f9892e..629c5e7a6d 100644 --- a/src/sql/workbench/services/dialog/common/dialogTypes.ts +++ b/src/sql/workbench/services/dialog/common/dialogTypes.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { DialogMessage, DialogWidth } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { DialogMessage, DialogWidth, DialogStyle, DialogPosition, IDialogProperties } from 'sql/workbench/api/common/sqlExtHostTypes'; export class ModelViewPane { private _valid: boolean = true; @@ -43,6 +43,11 @@ export class Dialog extends ModelViewPane { private static readonly CANCEL_BUTTON_LABEL = localize('dialogModalCancelButtonLabel', "Cancel"); public content: string | DialogTab[] = ''; + public dialogStyle: DialogStyle; + public dialogPosition: DialogPosition; + public renderHeader: boolean; + public renderFooter: boolean; + public dialogProperties: IDialogProperties; public okButton: DialogButton = new DialogButton(Dialog.DONE_BUTTON_LABEL, true); public cancelButton: DialogButton = new DialogButton(Dialog.CANCEL_BUTTON_LABEL, true); public customButtons: DialogButton[] = []; @@ -51,11 +56,26 @@ export class Dialog extends ModelViewPane { private _message: DialogMessage | undefined; private _closeValidator: CloseValidator | undefined; - constructor(public title: string, public width: DialogWidth, content?: string | DialogTab[]) { + constructor(public title: string, public width: DialogWidth, dialogStyle?: DialogStyle, dialogPosition?: DialogPosition, renderHeader?: boolean, renderFooter?: boolean, dialogProperties?: IDialogProperties, content?: string | DialogTab[]) { super(); if (content) { this.content = content; } + if (dialogStyle) { + this.dialogStyle = dialogStyle; + } + if (dialogPosition) { + this.dialogPosition = dialogPosition; + } + if (renderHeader) { + this.renderHeader = renderHeader; + } + if (renderFooter) { + this.renderFooter = renderFooter; + } + if (dialogProperties) { + this.dialogProperties = dialogProperties; + } } public get message(): DialogMessage | undefined { diff --git a/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts b/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts index 58e62f5cea..59b3a203af 100644 --- a/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts +++ b/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts @@ -52,7 +52,7 @@ export class ErrorMessageDialog extends Modal { @ILogService logService: ILogService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService ) { - super('', TelemetryKeys.ErrorMessage, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); + super('', TelemetryKeys.ErrorMessage, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { dialogStyle: 'normal', hasTitleIcon: true }); this._okLabel = localize('errorMessageDialog.ok', "OK"); this._closeLabel = localize('errorMessageDialog.close', "Close"); } diff --git a/src/sql/workbench/services/fileBrowser/browser/fileBrowserDialog.ts b/src/sql/workbench/services/fileBrowser/browser/fileBrowserDialog.ts index 8188fcfbf3..9a035e9b36 100644 --- a/src/sql/workbench/services/fileBrowser/browser/fileBrowserDialog.ts +++ b/src/sql/workbench/services/fileBrowser/browser/fileBrowserDialog.ts @@ -59,7 +59,7 @@ export class FileBrowserDialog extends Modal { @ILogService logService: ILogService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService ) { - super(title, TelemetryKeys.Backup, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { isFlyout: true, hasTitleIcon: false, hasBackButton: true, hasSpinner: true }); + super(title, TelemetryKeys.Backup, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { dialogStyle: 'flyout', hasTitleIcon: false, hasBackButton: true, hasSpinner: true }); this._viewModel = this._instantiationService.createInstance(FileBrowserViewModel); this._viewModel.onAddFileTree(args => this.handleOnAddFileTree(args.rootNode, args.selectedNode, args.expandedNodes).catch(err => onUnexpectedError(err))); this._viewModel.onPathValidate(args => this.handleOnValidate(args.succeeded, args.message)); diff --git a/src/sql/workbench/services/profiler/browser/profilerFilterDialog.ts b/src/sql/workbench/services/profiler/browser/profilerFilterDialog.ts index a04941ba99..3b9f9691fa 100644 --- a/src/sql/workbench/services/profiler/browser/profilerFilterDialog.ts +++ b/src/sql/workbench/services/profiler/browser/profilerFilterDialog.ts @@ -83,7 +83,7 @@ export class ProfilerFilterDialog extends Modal { @IProfilerService private profilerService: IProfilerService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService ) { - super('', TelemetryKeys.ProfilerFilter, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); + super('', TelemetryKeys.ProfilerFilter, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { dialogStyle: 'normal', hasTitleIcon: true }); } public open(input: ProfilerInput) { diff --git a/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialog.ts b/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialog.ts index 8ef1be4c6e..1a96e83249 100644 --- a/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialog.ts +++ b/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialog.ts @@ -87,7 +87,7 @@ export class FirewallRuleDialog extends Modal { textResourcePropertiesService, contextKeyService, { - isFlyout: true, + dialogStyle: 'flyout', hasBackButton: true, hasSpinner: true } diff --git a/src/sql/workbench/test/electron-browser/api/mainThreadModelViewDialog.test.ts b/src/sql/workbench/test/electron-browser/api/mainThreadModelViewDialog.test.ts index e352022546..bbf3e74db1 100644 --- a/src/sql/workbench/test/electron-browser/api/mainThreadModelViewDialog.test.ts +++ b/src/sql/workbench/test/electron-browser/api/mainThreadModelViewDialog.test.ts @@ -112,6 +112,11 @@ suite('MainThreadModelViewDialog Tests', () => { dialogDetails = { title: 'dialog1', width: 'narrow', + dialogStyle: 'callout', + dialogPosition: 'left', + renderHeader: true, + renderFooter: true, + dialogProperties: { xPos: 1200, yPos: 100, width: 20, height: 20 }, content: [tab1Handle, tab2Handle], okButton: okButtonHandle, cancelButton: cancelButtonHandle, diff --git a/test/automation/src/sql/notebook.ts b/test/automation/src/sql/notebook.ts index 30f08fdf62..0283c3425a 100644 --- a/test/automation/src/sql/notebook.ts +++ b/test/automation/src/sql/notebook.ts @@ -61,7 +61,7 @@ export class Notebook { async clearResults(): Promise { await this.code.waitAndClick('.notebookEditor'); - const clearResultsButton = '.editor-toolbar a[class="action-label codicon notebook-button icon-clear-results masked-icon"]'; + const clearResultsButton = '.editor-toolbar a[class="action-label codicon icon-clear-results masked-icon"]'; await this.code.waitAndClick(clearResultsButton); } @@ -153,10 +153,10 @@ export class Notebook { export class NotebookToolbar { private static readonly toolbarSelector = '.notebookEditor .editor-toolbar .actions-container'; - private static readonly toolbarButtonSelector = `${NotebookToolbar.toolbarSelector} a.action-label.codicon.notebook-button.masked-icon`; - private static readonly trustedButtonClass = 'action-label codicon notebook-button masked-icon icon-shield'; + private static readonly toolbarButtonSelector = `${NotebookToolbar.toolbarSelector} a.action-label.codicon.masked-icon`; + private static readonly trustedButtonClass = 'action-label codicon masked-icon icon-shield'; private static readonly trustedButtonSelector = `${NotebookToolbar.toolbarSelector} a[class="${NotebookToolbar.trustedButtonClass}"]`; - private static readonly notTrustedButtonClass = 'action-label codicon notebook-button masked-icon icon-shield-x'; + private static readonly notTrustedButtonClass = 'action-label codicon masked-icon icon-shield-x'; private static readonly notTrustedButtonSelector = `${NotebookToolbar.toolbarSelector} a[class="${NotebookToolbar.notTrustedButtonClass}"]`; constructor(private code: Code) { }