focus on the Copilotv2 editor

This commit is contained in:
Ashley Stanton-Nurse 2024-03-28 15:26:13 -07:00
parent 2598760a11
commit 8b6d857ddb
17 changed files with 129 additions and 39 deletions

View File

@ -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;
}

View File

@ -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;
}>;

View File

@ -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<CommandButtonComponentProps> {

View File

@ -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<QueryEditorProps> = (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 <EditorReact
language={"sql"}
content={props.content}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing Query"}
lineNumbers={"on"}
onContentChanged={props.onContentChanged}
onContentSelected={props.onContentSelected}
configureEditor={configureEditor}
/>;
}

View File

@ -754,6 +754,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onRevertClick,
commandButtonLabel: label,
ariaLabel: label,

View File

@ -4,9 +4,11 @@
* and update any knockout observables passed from the parent.
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { keyMap } from "Common/KeyboardShortcuts";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { userContext } from "UserContext";
import * as React from "react";
import { GlobalHotKeys } from "react-hotkeys";
import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
@ -105,8 +107,13 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
},
};
const handlers = CommandBarUtil.createKeyboardHandlers(staticButtons.concat(contextButtons).concat(controlButtons));
return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
{/* 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' */}
<GlobalHotKeys keyMap={keyMap} handlers={handlers} allowChanges={true} />
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}

View File

@ -354,6 +354,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
keyboardShortcut: "NEW_QUERY",
onCommandClick: () => {
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);

View File

@ -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: () => <ConnectionStatus container={container} poolId={poolId} />,
};
};
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;
}

View File

@ -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<QueryCopilotProps> = ({ explorer }: Query
)}
<Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
<QueryEditor
content={query}
onContentChanged={(newQuery: string) => setQuery(newQuery)}
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
onExecuteQuery={() => OnExecuteQueryClick(useQueryCopilot as Partial<QueryCopilotState>)}
COPILOT
<EditorReact
language={"sql"}
content={query}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing Query"}
lineNumbers={"on"}
onContentChanged={(newQuery: string) => setQuery(newQuery)}
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
/>
<QueryCopilotResults />
</SplitterLayout>

View File

@ -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,

View File

@ -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,

View File

@ -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<IQueryTabComponen
buttons.push({
iconSrc: CancelQueryIcon,
iconAlt: label,
keyboardShortcut: "CANCEL_QUERY",
onCommandClick: () => this.queryAbortController.abort(),
commandButtonLabel: label,
ariaLabel: label,
@ -584,6 +586,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}
private getEditorAndQueryResult(): JSX.Element {
const configureEditor = (monaco: MonacoNamespace, editor: monaco.editor.IStandaloneCodeEditor) => {
editor.addAction({
id: "execute-query",
label: "Execute Query",
keybindings: [monaco.KeyMod.Shift | monaco.KeyCode.Enter],
run: () => this.onExecuteQueryClick(),
});
}
return (
<Fragment>
<div className="tab-pane" id={this.props.tabId} role="tabpanel">
@ -599,12 +610,17 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
<Fragment>
<div className="queryEditor" style={{ height: "100%" }}>
<QueryEditor
<EditorReact
language={"sql"}
content={this.setEditorContent()}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing Query"}
lineNumbers={"on"}
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
onContentSelected={(selectedContent: string) => this.onSelectedContent(selectedContent)}
onExecuteQuery={() => this.onExecuteQueryClick()}
/>
configureEditor={configureEditor}
/>;
</div>
</Fragment>
{this.props.isSampleCopilotActive ? (

View File

@ -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,

View File

@ -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,

View File

@ -256,6 +256,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this,
iconSrc: DiscardIcon,
iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onDiscard,
commandButtonLabel: label,
ariaLabel: label,

View File

@ -4,9 +4,9 @@ import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@ -109,6 +109,7 @@ export default class UserDefinedFunctionTabContent extends Component<
...this,
iconSrc: DiscardIcon,
iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onDiscard,
commandButtonLabel: label,
ariaLabel: label,

View File

@ -18,6 +18,7 @@ import "../externals/jquery.typeahead.min.js";
// Image Dependencies
import { Platform } from "ConfigContext";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import * as ReactHotkeys from "react-hotkeys";
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import "../images/favicon.ico";
@ -61,6 +62,28 @@ import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
initializeIcons();
const tagsIgnoredByReactHotkeys = ["INPUT", "SELECT"];
ReactHotkeys.configure({
ignoreEventsCondition: (evt) => {
// 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<boolean>(true);
const isCarouselOpen = useCarousel((state) => state.shouldOpen);