From 8b6d857ddbbefb3cacb9f5e8c2c35185fb9e6dfc Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Thu, 28 Mar 2024 15:26:13 -0700 Subject: [PATCH] focus on the Copilotv2 editor --- less/documentDB.less | 5 ++++ src/Common/KeyboardShortcuts.ts | 25 +++++++++++++++++++ .../CommandButton/CommandButtonComponent.tsx | 9 ++++++- src/Explorer/Controls/Editor/QueryEditor.tsx | 22 ---------------- .../Controls/Settings/SettingsComponent.tsx | 1 + .../CommandBar/CommandBarComponentAdapter.tsx | 7 ++++++ .../CommandBarComponentButtonFactory.tsx | 2 ++ .../Menus/CommandBar/CommandBarUtil.tsx | 14 +++++++++++ src/Explorer/QueryCopilot/QueryCopilotTab.tsx | 17 ++++++++----- src/Explorer/Tabs/ConflictsTab.ts | 11 ++++---- src/Explorer/Tabs/DocumentsTab.ts | 2 ++ .../Tabs/QueryTab/QueryTabComponent.tsx | 24 +++++++++++++++--- src/Explorer/Tabs/ScriptTabBase.ts | 1 + .../StoredProcedureTabComponent.tsx | 1 + src/Explorer/Tabs/TriggerTabContent.tsx | 1 + .../Tabs/UserDefinedFunctionTabContent.tsx | 3 ++- src/Main.tsx | 23 +++++++++++++++++ 17 files changed, 129 insertions(+), 39 deletions(-) create mode 100644 src/Common/KeyboardShortcuts.ts diff --git a/less/documentDB.less b/less/documentDB.less index 1488df12a..357d159b3 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2302,6 +2302,11 @@ a:link { line-height: 22px; } +.monaco-editor .quick-input-list .highlight { + /* Padding in highlighted text within the quick input list breaks the flow of the text */ + padding: 0; +} + td a { color: #393939; } diff --git a/src/Common/KeyboardShortcuts.ts b/src/Common/KeyboardShortcuts.ts new file mode 100644 index 000000000..fa2e8e657 --- /dev/null +++ b/src/Common/KeyboardShortcuts.ts @@ -0,0 +1,25 @@ +import { KeyMap } from "react-hotkeys"; + +export const keyMap: KeyMap = { + NEW_QUERY: { + name: "New Query", + sequence: "ctrl+j", + action: "keydown", + }, + CANCEL_QUERY: { + name: "Cancel Query", + sequence: "f8", + action: "keydown", + }, + DISCARD: { + name: "Discard Changes", + sequence: "ctrl+x", + action: "keydown" + } +}; + +export type KeyboardShortcutName = keyof typeof keyMap; + +export type KeyboardShortcutHandlers = Partial<{ + [key in KeyboardShortcutName]: (keyEvent?: KeyboardEvent) => void; +}>; diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index 1e5cfc171..7345cc70a 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -1,6 +1,7 @@ /** * React component for Command button component. */ +import { KeyboardShortcutName } from "Common/KeyboardShortcuts"; import * as React from "react"; import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import { KeyCodes } from "../../../Common/Constants"; @@ -30,7 +31,7 @@ export interface CommandButtonComponentProps { /** * Click handler for command button click */ - onCommandClick: (e: React.SyntheticEvent) => void; + onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void; /** * Label for the button @@ -107,10 +108,16 @@ export interface CommandButtonComponentProps { * Vertical bar to divide buttons */ isDivider?: boolean; + /** * Aria-label for the button */ ariaLabel: string; + + /** + * A keyboard shortcut that can be used to activate this button. + */ + keyboardShortcut?: KeyboardShortcutName; } export class CommandButtonComponent extends React.Component { diff --git a/src/Explorer/Controls/Editor/QueryEditor.tsx b/src/Explorer/Controls/Editor/QueryEditor.tsx index 38942fbf0..d43537fd4 100644 --- a/src/Explorer/Controls/Editor/QueryEditor.tsx +++ b/src/Explorer/Controls/Editor/QueryEditor.tsx @@ -1,5 +1,3 @@ -import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; -import { MonacoNamespace, monaco } from "Explorer/LazyMonaco"; import React from "react"; export type QueryEditorProps = { @@ -14,24 +12,4 @@ export type QueryEditorProps = { }; export const QueryEditor: React.FunctionComponent = (props) => { - const configureEditor = (monaco: MonacoNamespace, editor: monaco.editor.IStandaloneCodeEditor) => { - editor.addAction({ - id: "execute-query", - label: "Execute Query", - keybindings: [monaco.KeyMod.Shift | monaco.KeyCode.Enter], - run: props.onExecuteQuery, - }); - } - - return ; } diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 574d75603..dbb3f18f8 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -754,6 +754,7 @@ export class SettingsComponent extends React.Component = ({ container }: Props) => { }, }; + const handlers = CommandBarUtil.createKeyboardHandlers(staticButtons.concat(contextButtons).concat(controlButtons)); + return (
+ {/* Handles keyboard shortcuts for command bar buttons when focus is OUTSIDE monaco. Even though it's placed here in the DOM, it hooks keydown on 'document' */} + + { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); @@ -369,6 +370,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB id: "newQueryBtn", iconSrc: AddSqlQueryIcon, iconAlt: label, + keyboardShortcut: "NEW_QUERY", onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 384bd06cd..2621c6120 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -6,6 +6,7 @@ import { IDropdownOption, IDropdownStyles, } from "@fluentui/react"; +import { KeyboardShortcutHandlers } from "Common/KeyboardShortcuts"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import _ from "underscore"; @@ -233,3 +234,16 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType, onRender: () => , }; }; + +export const createKeyboardHandlers = (buttons: CommandButtonComponentProps[]): KeyboardShortcutHandlers => { + const handlers: KeyboardShortcutHandlers = {}; + buttons.forEach((button) => { + if (button.keyboardShortcut) { + handlers[button.keyboardShortcut] = (e) => { + button.onCommandClick(e); + e.preventDefault(); + }; + } + }); + return handlers; +} diff --git a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx index 6010e39bf..8dadb7e34 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx @@ -2,7 +2,7 @@ import { Stack } from "@fluentui/react"; import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; -import { QueryEditor } from "Explorer/Controls/Editor/QueryEditor"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane"; import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar"; @@ -104,11 +104,16 @@ export const QueryCopilotTab: React.FC = ({ explorer }: Query )} - setQuery(newQuery)} - onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)} - onExecuteQuery={() => OnExecuteQueryClick(useQueryCopilot as Partial)} + COPILOT + setQuery(newQuery)} + onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)} /> diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index c01c206c1..12a04816b 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -6,16 +6,16 @@ import DiscardIcon from "../../../images/discard.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; import * as Constants from "../../Common/Constants"; import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants"; -import { createDocument } from "../../Common/dataAccess/createDocument"; -import { deleteConflict } from "../../Common/dataAccess/deleteConflict"; -import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; -import { queryConflicts } from "../../Common/dataAccess/queryConflicts"; -import { updateDocument } from "../../Common/dataAccess/updateDocument"; import editable from "../../Common/EditableUtility"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import * as HeadersUtility from "../../Common/HeadersUtility"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; +import { createDocument } from "../../Common/dataAccess/createDocument"; +import { deleteConflict } from "../../Common/dataAccess/deleteConflict"; +import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; +import { queryConflicts } from "../../Common/dataAccess/queryConflicts"; +import { updateDocument } from "../../Common/dataAccess/updateDocument"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; @@ -621,6 +621,7 @@ export default class ConflictsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardShortcut: "DISCARD", onCommandClick: this.onDiscardClick, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 3591378da..4ac6ae205 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -921,6 +921,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardShortcut: "DISCARD", onCommandClick: this.onRevertNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -950,6 +951,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardShortcut: "DISCARD", onCommandClick: this.onRevertExisitingDocumentClick, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 111abcbeb..4fca54760 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -3,7 +3,8 @@ import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; -import { QueryEditor } from "Explorer/Controls/Editor/QueryEditor"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; +import { MonacoNamespace, monaco } from "Explorer/LazyMonaco"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar"; @@ -468,6 +469,7 @@ export default class QueryTabComponent extends React.Component this.queryAbortController.abort(), commandButtonLabel: label, ariaLabel: label, @@ -584,6 +586,15 @@ export default class QueryTabComponent extends React.Component { + editor.addAction({ + id: "execute-query", + label: "Execute Query", + keybindings: [monaco.KeyMod.Shift | monaco.KeyCode.Enter], + run: () => this.onExecuteQueryClick(), + }); + } + return (
@@ -599,12 +610,17 @@ export default class QueryTabComponent extends React.Component
- this.onChangeContent(newContent)} onContentSelected={(selectedContent: string) => this.onSelectedContent(selectedContent)} - onExecuteQuery={() => this.onExecuteQueryClick()} - /> + configureEditor={configureEditor} + />;
{this.props.isSampleCopilotActive ? ( diff --git a/src/Explorer/Tabs/ScriptTabBase.ts b/src/Explorer/Tabs/ScriptTabBase.ts index 240941390..ad176c555 100644 --- a/src/Explorer/Tabs/ScriptTabBase.ts +++ b/src/Explorer/Tabs/ScriptTabBase.ts @@ -238,6 +238,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardShortcut: "DISCARD", onCommandClick: this.onDiscard, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index 98e398e1b..9202ef0b0 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -347,6 +347,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardShortcut: "DISCARD", onCommandClick: this.onDiscard, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index bf756598e..81578c9a8 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -256,6 +256,7 @@ export class TriggerTabContent extends Component { + // The default react-hotkeys behavior is to ignore events targetting a textarea, but we want the monaco editor's key events to bubble up + // So, we configure it to ignore all events targetting a textarea except when the target is a monaco editor's text area + + if (!(evt.target instanceof HTMLElement)) { + return true; + } + + if (tagsIgnoredByReactHotkeys.includes(evt.target.tagName)) { + return true; + } + + if (evt.target.tagName === "TEXTAREA" && !evt.target.matches(".monaco-editor textarea")) { + return true; + } + + return false; + } +}) + const App: React.FunctionComponent = () => { const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState(true); const isCarouselOpen = useCarousel((state) => state.shouldOpen);