From 05f97411fa73c54f887552d9c38dffc9550ba03a Mon Sep 17 00:00:00 2001 From: Hale Rankin Date: Tue, 16 Feb 2021 16:58:55 -0800 Subject: [PATCH] New feature for Notebooks - Callout (#13078) * New feature for Notebooks - callout. This utilizes a modifed modal.ts that renders a small modal set to appear at the trigger event and disappear when user clicks off of it. This is intended only for micro-interactions such as inserting links, tables and images into Notebooks. Error dialogs dependent on modal have been updated to implement the modified width property correctlty. * Modified dialogStyle names in interface. Applied updates to files dependent on this. Wired up disposable listener for browsing local files in image select callout. Renamed callout to calloutDialog. * Converted double quotes in CSS to single quotes. Revised run method, removing the promise wrapping from the conditional. * Passing click target to modal for positioning. Created custom buttons and click event handlers for Insert image and Insert link. Set feature behind preview flag. * Revised structure and styles. * Updated component for use with ML extension. Updated ML extension code with changes for custom dialog creation. * Fixed async context for handleBrowse. Passing the new modal properties into Dialog implementation for ML. * Added option to suppress callout dialog header and footer from the machine learning view file. Added a compact variant of callout. Corrected Dialog tab initialization. * Maddy/callout modifications (#13586) * initial changes * canSelectFolders false * change label, placeholder text on radiobtn click * Added support for custom XY offset to account for modal size and to provide for fine-tuning of unique modal instances. * Updated Image icon with latest from Design. * Replaced node process with IPathService for retreiving image file. * Added theme color lookups to provide default colors when none are provided. * Added async/await to tests calling transformText. * textCell, modal - swapped out HostListener for a member method which listens for the Esc keyup. Updated templates with method call on keyup. This cleaned up the double event call whenever the Esc key was pressed. Added property to modal Cancel button call to make it a secondary button. * Cleaned up callout styles. * Removed color use for input fields because component code delivers theme-specific values. * Added check for CSS class `mac` on the body tag. Added conditionals to supply alternate dialogXYOffset in the case of non-Mac environment. * Cleanup: Simplified DialogPosition. Renamed telemtry references and dialog related methods. Corrected spelling. Added missing signatures and types. Updated warning callout link URLs. * Removed async and await as there are no promise dependencies. Added a signature. * Revert "Fix windows insiders icons (#13579)" (#13630) This reverts commit a0ef5947924425fd2cf17e714164872af4fe824a. * Changed cores validation message (#13617) * Changed cores validation message * Missed validation * Remove cores validation message * Applied verification for cores change to miaa c+s page * WYSIWYG Improvements to highlight (#13032) * Improvements to highlight * wip * Tests pass * Leverage escaping mechanism * Tweak highlight logic * PR comments * add await to thenable method (#13635) * Delete ConnectionDialogue.ipynb (#13634) this nb was an attempt at creating a connection dialog. removing not found in toc * Added engine version argument to edit command. (#13610) * Added engine version argument to edit command. Neccessary for not using pg12 * Included for changing password in overview page * Updated fakeazdataapi test * Fix empty column issue (#13641) Co-authored-by: Monica Gupta * add right padding to notebook toolbar action item (#13640) * add right padding to action item * remove extra line and add space * Adding SQL Edge project template (#13558) * Checkpoint * removing flag for not creating subfolder * Adding Edge template * Removing janky map function * Adding templates for additional objects * Updating tests, fixing bug * Added Edge project icon * Updating strings to Drew-approved text * Cleaning up template scripts and Edge project template names * Update package.json (#13626) * Update Import UI to match other UIs (#13637) * Update Import UI to match other UIs * Fixed another bug * Fix notebook unordered grid values after papermill execution (#13614) * Fix unordered table * check entire first row schema: * SQL Notebooks should not get affected * delete unused variable and edit comments * refactor for efficient table ordering * nit naming * Normalize path to change (#13660) * vbump asde deployment extension (#13662) do a patch version update, will adjust if the next change is a major one. * Add test for dynamic enablement (#13602) * Add test for dynamic enablement * update names * Remove debug console log (#13669) * Remove placeholder on deployment wizards when field is disabled dynamically (#13658) * Bump highlight.js in /extensions/markdown-language-features (#13675) Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 9.15.10 to 10.4.1. - [Release notes](https://github.com/highlightjs/highlight.js/releases) - [Changelog](https://github.com/highlightjs/highlight.js/blob/master/CHANGES.md) - [Commits](https://github.com/highlightjs/highlight.js/compare/9.15.10...10.4.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * add ability to control the enabled state of checkbox cells (#13644) * control enabled state of checkbox cells * add more check * Prevent Table from Disappearing due to exception when looking for tHead (#13680) * Prevent exception when tHead doesn't exist at node * Add test for no thead * Fix issue with pasting results in Teams (#13673) * Fix issue with pasting results in Teams * Addressed comment to change header tag to th Co-authored-by: Monica Gupta * Add descriptions and validation to connected mode (#13676) * Add dependent field provider to resource deployment (#13664) * Add dependent field provider to resource deployment * Change name to value provider service * Add error handling * providerId -> id * Set dropdown value correctly * missed id * back to providerId * fix updating missed id * remove placeholder * Have resource deployment providers return disposables (#13690) * Add dependent field provider to resource deployment * Change name to value provider service * Add error handling * providerId -> id * Set dropdown value correctly * missed id * back to providerId * fix updating missed id * Make resource deployment providers disposable * Retry publish and always try adding asset (#13700) * Retry publish and always try adding asset * Undo asset upload change * Add logging * Notebooks: Remove result set summary from saved metadata (#13616) * remove result set summary from metadata * remove batchId and id from celloutputmetadata * remove extra line * Add scan suppressions (#13705) * Add action for responding to Needs Logs label (#13707) * Fix action name (#13708) * Add action for responding to Needs Logs label * Fix action name * Rename action config file (#13709) * Add action for responding to Needs Logs label * Fix action name * Rename config file * remove quotes * Adding unit tests for schema compare service (#13642) * Retry getConfig (#13712) * Retry getConfig * Add logging * vBump Arc and Azdata (#13717) * switch schema compare to use inputbox instead of table headers (#13715) * Added Accounts and Database Backup Page to Migration wizard (#13548) * Added localized strings Created a db backup page added radio buttons * created components for database backup page * Added account selection page * Added accounts page * Some more work done - Added page validations - Almost done with db backup except for a few api calls. * Some more progress added graph api for storage account * Finished hooking up all the endpoints on db page. * Some code fixed and refactoring * Fixed a ton of validation bugs * Added common localized strings to the constants file * some code cleanup * changed method name to makeHttpGetRequest * change http result class name * Added return types and return values to the functions * removed void returns * Added more return types and values * Storing accounts in the map with ids as key Fixed a bug in case of no subscriptions found * cleaning up the code * Fixed localized strings * Added comments to get request api Added validation logic to database backup page removed unnecessary page validations. * Added some get resource functions in azure core * Changed thenable to promise * Added arm calls for file shares and blob storage * Added field specific validation error message * Added examples in validation error message. * Fixed some typings and localized string * Added live validations to dropdowns * Fixed method name to getSQLVMservers * Use console.log for retry logging (#13722) * Fixed Schema compare integration tests by adding retry (#13649) * Add workspace information in Import UI (#13648) * Add workspace information in Import UI * Addressed comments * Reduced space between Workspace heading and the label * Lint azdata.d.ts (#13728) * Added developer name to the list of developers. (#13725) onboarding commit: Added developer name to the list. * Fix environment variables for controller create (#13732) * vbump schema compare and sql database projects (#13730) * December release readme (#13733) * Change server group look (#13608) * change server group look * remove dead code * add top padding * add bot padding as well * fix heights to account for padding * fix arrow alignment * fix ellipses and node length parity * fix alignment * Make loading components not valid and improve RD radio group (#13738) * Revert "Added Accounts and Database Backup Page to Migration wizard (#13548)" (#13742) This reverts commit e1690055712c48fb860fdcc1350c72f07440f186. * Add loading text properties for option sources (#13743) * Add loading text to deployment radio options * Fix loading race condition * Update text * Add kube config and kube cluster to arc data controller screens (#13551) * Un-skip and fix a few of the db projects tests (#13726) * Un-skip and fix a few of the db projects tests * Addressed comments * Fix one test failure on Linux/Mac * Update STS to revert SqlClient update (#13758) * Update required azdata versions (#13762) * fix the recent list (#13770) * Adding base classes for data dev extension telemetry (#13763) * adding telemetry dependencies for data-workspaces and sql-database-projects * Adding telemetry dependencies for dacpac extension * Adding telemetry base to data workspaces and projects * Adding telemetry base code to the dacpac extension Co-authored-by: Benjin Dubishar Co-authored-by: Sai Avishkar Sreerama * Update changelog (#13773) * Notebook Extension: First logging improvements (#13729) * First logging improvements * PR feedback for err output * Add BEGIN/END to snippet (#13784) * Adding database backup and accounts page to migration wizard (#13764) * Added localized strings Created a db backup page added radio buttons * created components for database backup page * Added account selection page * Added accounts page * Some more work done - Added page validations - Almost done with db backup except for a few api calls. * Some more progress added graph api for storage account * Finished hooking up all the endpoints on db page. * Some code fixed and refactoring * Fixed a ton of validation bugs * Added common localized strings to the constants file * some code cleanup * changed method name to makeHttpGetRequest * change http result class name * Added return types and return values to the functions * removed void returns * Added more return types and values * Storing accounts in the map with ids as key Fixed a bug in case of no subscriptions found * cleaning up the code * Fixed localized strings * Added comments to get request api Added validation logic to database backup page removed unnecessary page validations. * Added some get resource functions in azure core * Changed thenable to promise * Added arm calls for file shares and blob storage * Added field specific validation error message * Added examples in validation error message. * Fixed some typings and localized string * Added live validations to dropdowns * Fixed method name to getSQLVMservers * Using older storage package * Update typings/namings (#13767) * Update typings * more typings fixes * switched fileshares and blobcontainers api to http requests * removed the extra line * Adding resource graph documentation link as a comment * remove makeHttpRequest api from azurecore Co-authored-by: Charles Gagnon * change afterClean to beforeBuild for removing the generated assests.json (#13736) * change afterClean to beforeBuild for removing the generated assests.json * fix merge conflict * rename files * Clean up Loading Component typings (#13785) * Clean up Loading Component typings * add properties to impl * Log active element when notebook smoke test fails (#13724) * add error log for active element * fix active element selector * await isActiveElement call * call waitforactiveleement * Bump ini from 1.3.5 to 1.3.7 in /samples/sqlservices (#13776) Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ini from 1.3.5 to 1.3.7 in /build (#13765) Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ini from 1.3.4 to 1.3.8 (#13792) Bumps [ini](https://github.com/isaacs/ini) from 1.3.4 to 1.3.8. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.4...v1.3.8) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ini from 1.3.5 to 1.3.8 in /extensions/markdown-language-features (#13791) Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * renable kernels dropdown test (#13727) Verify no skipped core tests * Remove redundant parameter in test scripts (#13755) * Fire onDidSelect event when selecting event from code (#13691) * Fire onDidSelect event when selecting event from code * Fix import tests * fix typo * Use correct Azure graph endpoint & cleanup (#13786) * Use correct Azure graph endpoint * Add enum * Update CODEOWNERS (#13798) * Make project workspace selectable if no workspace is open yet (#13508) * allow new workspace location to be editable * fix workspace inputbox not showing up after toggling open workspace radio buttons * add a few tests * cleanup * fix errors * addressing comments * fix filter for windows * add error message if existing workspace file is selected and change picker to be folder only * address comments * fix typos and update tests * vbump 2018 -> 2019 (#13800) * Fix "not externalized correctly" warnings (#13806) * Notebook Deep Link to Section (#13795) * Notebook deep link to section * fragment wip * table component improvement (#13801) * hyperlink column * fixed width for image only button - old behavior * Server Reports extension: fix for start and stop xevent sessions (#13565) * fix for start and stop xevent sessions * vscode.open for help URL * setup info messages for localization @kburtram - I could use an assist on updating the v# and publishing the vsix, but there's no rush this can wait until after the holidays * Arc - Update Postgres name length limit (#13807) Arc - Update Postgres name length limit. It was recently reduced from 12 to 11. * Fix whitespace differences in sqlproj (#13805) * add whiteSpaceAtEndOfSelfclosingTag * update test baselines * Fix paths for tests (#13816) * Fixed the stray validation error message in Resource Deployment wizard (#13747) * Fixed the stray validation error message * Removed not working ' with validation Adding back cancel button disabling * mark a couple data workspace tests as unstable (#13822) * cosmetic changes (#13820) * cosmetic changes * moved limitLongName function to the utils * add . as trigger character (#13811) * Remove hardcoded search box height (#13823) * Use azdata-test modelview stubs (#13818) * Filter vscode delegate command events (#13832) * Fix duplicate SVG rendering (#13828) * Fix select box event ordering (#13831) * Fix select box event ordering * more fixes * Fix page * Revert typing change * Undo param * Fix compile error * Completely remove typings * Dacpac - Showing error message to user if operation fails (#13830) * Showing error message to user if operation fails. * Added more tests * Fix unstable data-workspace tests (#13824) * stub file existing validation * add error message * change back to calling dialog.validate() * move tests to separate dialogbase file and add more error message validation * comment out the unstable unit test step (#13834) * remove --build flag * comment out unstable test * add build tag back * Removed padding-top / bottom declarations for text cell notebook-preview. This compacts the text cell by 14px on top and bottom. (#13815) * Stop forcing left text align on tables (#13840) * Have same connection logic for all nb int tests (#13844) * Added fix for the infinite page refresh in resource Deployment tools page (#13813) * Added fixed for the infinite loop in resource Deployment tools page Generating events for select boxes only when the select box value is changed. * Fixed the check logic in select method * Reverted to old code and fixed some bugs * Fixed event generation check logic * Make new workspace inputbox editable in Create project from database dialog (#13842) * update create project from database dialog to have editable new workspace * add validation * add test * add error message * Remove test for now * cleanup * add periods * throw errors * change return type to void * Removal of Components folder, moving ADP notebook to its own notebook. (#13848) * adp folder removed, notebook moved. * moved ADP notebook to own folder. * Passing click target to modal for positioning. Created custom buttons and click event handlers for Insert image and Insert link. Set feature behind preview flag. * Revert "Revert "Fix windows insiders icons (#13579)" (#13630)" This reverts commit 111dcb4c6885d9ab1a24398b13f103835789e6e0. * Corrected button style and declared dialogStyle for ManageModelsDialog. This fixes broken layout see when user clicks: Import or view models. * Revised suppressHeader/Footer to renderHeader/Footer and fixed logic. Code cleanup. Changed how calloutType: IMAGE and LINK are implemented. Added comments where neeed. * Fixed callout separator: Swapped out transparent for notebookToolbarLines * Removed promise context from image callout logic. * Moved calloutDialog into modal folder. * Code and style adjustments per feedback. Removed dependency on mac body class. Cleaned up dialog theme. Revsied modal callout logic. * Corrected CSS for notebook toolbar. Removed unused code. Added code to ensure that renderFooter would occur whether true or undefined. * Code cleanup. Clarified more details in my code comments. Added comments to values that needed identification. Escaping user-supplied text before it is rendered. * Renamed TriggerProperties interface and implementations to DialogProperties. Added default value for DialogStyle to sqlExtHost.api.impl so existing dialogs take flyout by default. * Replaced null value with undefined. Revised theme logic to account for undefined color value. * Fixed top 30px offset rule so callout dialogs do not get this added. * Revised undefined check for modal theme color. Removed calloutCompact. Moved callout dialog widths into DialogWidth property when calling methods to create callout or dialog. Added comments. Revised CSS. * Providing fallback value in the case of null. Hex: FFFFFF00 is white with no transparency. * Removed fallback hex color for foreground. Added check for foregroundRgb before attempting to grab rgba values. Added footer top border color. * Added formatting after resolving conflicts. * Corrected implementation after taking changes from main. Co-authored-by: Maddy <12754347+MaddyDev@users.noreply.github.com> Co-authored-by: Charles Gagnon Co-authored-by: nasc17 <69922333+nasc17@users.noreply.github.com> Co-authored-by: Chris LaFreniere <40371649+chlafreniere@users.noreply.github.com> Co-authored-by: Barbara Valdez <34872381+barbaravaldez@users.noreply.github.com> Co-authored-by: Christopher C <37060219+cavonac@users.noreply.github.com> Co-authored-by: Monica Gupta Co-authored-by: Monica Gupta Co-authored-by: Benjin Dubishar Co-authored-by: Karl Burtram Co-authored-by: Sakshi Sharma <57200045+SakshiS-harma@users.noreply.github.com> Co-authored-by: Vasu Bhog Co-authored-by: Alan Ren Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lucy Zhang Co-authored-by: Leila Lali Co-authored-by: Kim Santiago <31145923+kisantia@users.noreply.github.com> Co-authored-by: Aasim Khan Co-authored-by: Sai Avishkar Sreerama <74571829+ssreerama@users.noreply.github.com> Co-authored-by: Aditya Bist Co-authored-by: Arvind Ranasaria Co-authored-by: Benjin Dubishar Co-authored-by: Sai Avishkar Sreerama Co-authored-by: Drew Skwiers-Koballa Co-authored-by: Brian Bergeron Co-authored-by: Vladimir Chernov Co-authored-by: Alex Ma --- .../machine-learning/src/common/apiWrapper.ts | 4 +- .../machine-learning/src/common/constants.ts | 5 + .../models/manageModels/manageModelsDialog.ts | 1 + .../views/models/prediction/columnsTable.ts | 76 +++- .../views/models/tableSelectionComponent.ts | 1 - src/sql/azdata.proposed.d.ts | 65 +++- src/sql/media/icons/toolbar-image.svg | 6 +- .../telemetry/common/telemetryKeys.ts | 1 + .../api/browser/mainThreadModelViewDialog.ts | 21 +- .../api/common/extHostModelViewDialog.ts | 106 +++++- .../api/common/sqlExtHost.api.impl.ts | 9 +- .../workbench/api/common/sqlExtHostTypes.ts | 16 + .../workbench/browser/modal/calloutDialog.ts | 326 ++++++++++++++++++ .../browser/modal/media/browse-local.svg | 4 + .../workbench/browser/modal/media/modal.css | 172 +++++++-- src/sql/workbench/browser/modal/modal.ts | 213 +++++++++--- .../cellViews/cellToolbar.component.ts | 6 +- .../browser/cellViews/codeCell.component.html | 2 +- .../browser/cellViews/codeCell.component.ts | 18 +- .../cellViews/markdownToolbar.component.ts | 100 +++++- .../browser/cellViews/textCell.component.html | 2 +- .../browser/cellViews/textCell.component.ts | 22 +- .../browser/markdownToolbarActions.ts | 88 ++++- .../notebook/browser/notebook.component.ts | 16 +- .../contrib/notebook/browser/notebook.css | 71 ++-- .../notebook/browser/notebookActions.ts | 6 +- .../notebook/browser/notebookStyles.ts | 6 +- .../browser/markdownTextTransformer.test.ts | 122 +++---- .../contrib/webview/browser/webViewDialog.ts | 2 +- .../browser/autoOAuthDialog.ts | 2 +- .../dialog/browser/customDialogService.ts | 6 + .../services/dialog/browser/dialogModal.ts | 24 +- .../services/dialog/common/dialogTypes.ts | 24 +- .../browser/errorMessageDialog.ts | 2 +- .../fileBrowser/browser/fileBrowserDialog.ts | 2 +- .../profiler/browser/profilerFilterDialog.ts | 2 +- .../browser/firewallRuleDialog.ts | 2 +- .../api/mainThreadModelViewDialog.test.ts | 5 + test/automation/src/sql/notebook.ts | 8 +- 39 files changed, 1274 insertions(+), 290 deletions(-) create mode 100644 src/sql/workbench/browser/modal/calloutDialog.ts create mode 100644 src/sql/workbench/browser/modal/media/browse-local.svg 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) { }