[Task 3061766] Global Keyboard Shortcuts, implemented through the Command Bar (#1789)
* keyboard shortcuts using tinykeys * refmt and fix lints * retarget keyboard shortcuts to the body instead of the root element of the React component tree * refmt * Update src/Explorer/Menus/CommandBar/CommandBarUtil.tsx Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com> * add Save binding to New Item command bar --------- Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
This commit is contained in:
parent
e0cb3da6aa
commit
a44ed1f45c
|
@ -106,6 +106,7 @@
|
||||||
"styled-components": "5.0.1",
|
"styled-components": "5.0.1",
|
||||||
"swr": "0.4.0",
|
"swr": "0.4.0",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
|
"tinykeys": "2.1.0",
|
||||||
"underscore": "1.9.1",
|
"underscore": "1.9.1",
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.10.0",
|
||||||
"zustand": "3.5.0"
|
"zustand": "3.5.0"
|
||||||
|
@ -37786,6 +37787,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
|
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/tinykeys": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinykeys/-/tinykeys-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-/MESnqBD1xItZJn5oGQ4OsNORQgJfPP96XSGoyu4eLpwpL0ifO0SYR5OD76u0YMhMXsqkb0UqvI9+yXTh4xv8Q=="
|
||||||
|
},
|
||||||
"node_modules/tinyqueue": {
|
"node_modules/tinyqueue": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz",
|
||||||
|
|
|
@ -101,6 +101,7 @@
|
||||||
"styled-components": "5.0.1",
|
"styled-components": "5.0.1",
|
||||||
"swr": "0.4.0",
|
"swr": "0.4.0",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
|
"tinykeys": "2.1.0",
|
||||||
"underscore": "1.9.1",
|
"underscore": "1.9.1",
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.10.0",
|
||||||
"zustand": "3.5.0"
|
"zustand": "3.5.0"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* React component for Command button component.
|
* React component for Command button component.
|
||||||
*/
|
*/
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||||
import { KeyCodes } from "../../../Common/Constants";
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
|
@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
|
||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick: (e: React.SyntheticEvent) => void;
|
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
|
@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
|
||||||
* Vertical bar to divide buttons
|
* Vertical bar to divide buttons
|
||||||
*/
|
*/
|
||||||
isDivider?: boolean;
|
isDivider?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aria-label for the button
|
* Aria-label for the button
|
||||||
*/
|
*/
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If specified, a keyboard action that should trigger this button's onCommandClick handler when activated.
|
||||||
|
* If not specified, the button will not be triggerable by keyboard shortcuts.
|
||||||
|
*/
|
||||||
|
keyboardAction?: KeyboardAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { useKeyboardActionHandlers } from "KeyboardShortcuts";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
|
@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
const buttons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
|
const setKeyboardShortcutHandlers = useKeyboardActionHandlers((state) => state.setHandlers);
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
const buttons =
|
const buttons =
|
||||||
|
@ -105,6 +107,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||||
|
setKeyboardShortcutHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||||
|
@ -297,6 +298,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||||
id: "newQueryBtn",
|
id: "newQueryBtn",
|
||||||
iconSrc: AddSqlQueryIcon,
|
iconSrc: AddSqlQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_QUERY,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||||
|
@ -312,6 +314,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||||
id: "newQueryBtn",
|
id: "newQueryBtn",
|
||||||
iconSrc: AddSqlQueryIcon,
|
iconSrc: AddSqlQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_QUERY,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||||
|
@ -397,6 +400,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
|
||||||
return {
|
return {
|
||||||
iconSrc: BrowseQueriesIcon,
|
iconSrc: BrowseQueriesIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.OPEN_QUERY,
|
||||||
onCommandClick: () =>
|
onCommandClick: () =>
|
||||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
|
@ -411,6 +415,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||||
return {
|
return {
|
||||||
iconSrc: OpenQueryFromDiskIcon,
|
iconSrc: OpenQueryFromDiskIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
|
||||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
IDropdownStyles,
|
IDropdownStyles,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { KeyboardHandlerMap } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||||
|
@ -233,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
||||||
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
||||||
|
const handlers: KeyboardHandlerMap = {};
|
||||||
|
|
||||||
|
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
if (!button.disabled && button.keyboardAction) {
|
||||||
|
handlers[button.keyboardAction] = (e) => {
|
||||||
|
button.onCommandClick(e);
|
||||||
|
|
||||||
|
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button.children && button.children.length > 0) {
|
||||||
|
createHandlers(button.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createHandlers(allButtons);
|
||||||
|
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
@ -907,6 +908,7 @@ export default class DocumentsTab extends TabsBase {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveNewDocumentClick,
|
onCommandClick: this.onSaveNewDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -936,6 +938,7 @@ export default class DocumentsTab extends TabsBase {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveExistingDocumentClick,
|
onCommandClick: this.onSaveExistingDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
@ -393,6 +394,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.EXECUTE_ITEM,
|
||||||
onCommandClick: this.props.isSampleCopilotActive
|
onCommandClick: this.props.isSampleCopilotActive
|
||||||
? () => OnExecuteQueryClick(this.props.copilotStore)
|
? () => OnExecuteQueryClick(this.props.copilotStore)
|
||||||
: this.onExecuteQueryClick,
|
: this.onExecuteQueryClick,
|
||||||
|
@ -408,6 +410,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveQueryIcon,
|
iconSrc: SaveQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveQueryClick,
|
onCommandClick: this.onSaveQueryClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -468,6 +471,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: CancelQueryIcon,
|
iconSrc: CancelQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_QUERY,
|
||||||
onCommandClick: () => this.queryAbortController.abort(),
|
onCommandClick: () => this.queryAbortController.abort(),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||||
import { Pivot, PivotItem } from "@fluentui/react";
|
import { Pivot, PivotItem } from "@fluentui/react";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
|
@ -321,6 +322,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveClick,
|
onCommandClick: this.onSaveClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -334,6 +336,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onUpdateClick,
|
onCommandClick: this.onUpdateClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -360,6 +363,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.EXECUTE_ITEM,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
this.collection.container.openExecuteSprocParamsPanel(this.node);
|
this.collection.container.openExecuteSprocParamsPanel(this.node);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { TriggerDefinition } from "@azure/cosmos";
|
import { TriggerDefinition } from "@azure/cosmos";
|
||||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
|
@ -227,6 +228,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||||
...this,
|
...this,
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveClick,
|
onCommandClick: this.onSaveClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -241,6 +243,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||||
...this,
|
...this,
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onUpdateClick,
|
onCommandClick: this.onUpdateClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { Label, TextField } from "@fluentui/react";
|
import { Label, TextField } from "@fluentui/react";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
|
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
|
||||||
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
|
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
@ -80,6 +81,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||||
setState: this.setState,
|
setState: this.setState,
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveClick,
|
onCommandClick: this.onSaveClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -94,6 +96,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||||
...this,
|
...this,
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onUpdateClick,
|
onCommandClick: this.onUpdateClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { PropsWithChildren, useEffect } from "react";
|
||||||
|
import { KeyBindingMap, tinykeys } from "tinykeys";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a keyboard shortcut handler.
|
||||||
|
* Return `true` to prevent the default action of the keyboard shortcut.
|
||||||
|
* Any other return value will allow the default action to proceed.
|
||||||
|
*/
|
||||||
|
export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void;
|
||||||
|
|
||||||
|
export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHandler>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible actions that can be triggered by keyboard shortcuts.
|
||||||
|
*/
|
||||||
|
export enum KeyboardAction {
|
||||||
|
NEW_QUERY = "NEW_QUERY",
|
||||||
|
EXECUTE_ITEM = "EXECUTE_ITEM",
|
||||||
|
CANCEL_QUERY = "CANCEL_QUERY",
|
||||||
|
SAVE_ITEM = "SAVE_ITEM",
|
||||||
|
OPEN_QUERY = "OPEN_QUERY",
|
||||||
|
OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The keyboard shortcuts for the application.
|
||||||
|
* This record maps each action to the keyboard shortcuts that trigger the action.
|
||||||
|
* Even if an action is specified here, it will not be triggered unless a handler is set for it.
|
||||||
|
*/
|
||||||
|
const bindings: Record<KeyboardAction, string[]> = {
|
||||||
|
// NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS.
|
||||||
|
// See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts.
|
||||||
|
|
||||||
|
[KeyboardAction.NEW_QUERY]: ["$mod+J"],
|
||||||
|
[KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"],
|
||||||
|
[KeyboardAction.CANCEL_QUERY]: ["Escape"],
|
||||||
|
[KeyboardAction.SAVE_ITEM]: ["$mod+S"],
|
||||||
|
[KeyboardAction.OPEN_QUERY]: ["$mod+O"],
|
||||||
|
[KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface KeyboardShortcutState {
|
||||||
|
/**
|
||||||
|
* A set of all the keyboard shortcuts handlers.
|
||||||
|
*/
|
||||||
|
allHandlers: KeyboardHandlerMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the keyboard shortcut handlers.
|
||||||
|
*/
|
||||||
|
setHandlers: (handlers: KeyboardHandlerMap) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set) => ({
|
||||||
|
allHandlers: {},
|
||||||
|
setHandlers: (handlers: Partial<Record<KeyboardAction, KeyboardActionHandler>>) => {
|
||||||
|
set({ allHandlers: handlers });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createHandler(action: KeyboardAction): KeyboardActionHandler {
|
||||||
|
return (e) => {
|
||||||
|
const state = useKeyboardActionHandlers.getState();
|
||||||
|
const handler = state.allHandlers[action];
|
||||||
|
if (handler && handler(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allHandlers: KeyBindingMap = {};
|
||||||
|
(Object.keys(bindings) as KeyboardAction[]).forEach((action) => {
|
||||||
|
const shortcuts = bindings[action];
|
||||||
|
shortcuts.forEach((shortcut) => {
|
||||||
|
allHandlers[shortcut] = createHandler(action);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export function KeyboardShortcutRoot({ children }: PropsWithChildren<unknown>) {
|
||||||
|
useEffect(() => {
|
||||||
|
// We bind to the body because Fluent UI components sometimes shift focus to the body, which is above the root React component.
|
||||||
|
tinykeys(document.body, allHandlers);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
89
src/Main.tsx
89
src/Main.tsx
|
@ -21,6 +21,7 @@ import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { Platform } from "ConfigContext";
|
import { Platform } from "ConfigContext";
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
|
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
import "../images/favicon.ico";
|
import "../images/favicon.ico";
|
||||||
|
@ -91,52 +92,54 @@ const App: React.FunctionComponent = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flexContainer" aria-hidden="false">
|
<KeyboardShortcutRoot>
|
||||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
<div className="flexContainer" aria-hidden="false">
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
{/* Main Command Bar - Start */}
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
<CommandBar container={explorer} />
|
{/* Main Command Bar - Start */}
|
||||||
{/* Collections Tree and Tabs - Begin */}
|
<CommandBar container={explorer} />
|
||||||
<div className="resourceTreeAndTabs">
|
{/* Collections Tree and Tabs - Begin */}
|
||||||
{/* Collections Tree - Start */}
|
<div className="resourceTreeAndTabs">
|
||||||
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
|
{/* Collections Tree - Start */}
|
||||||
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
|
||||||
<div className="collectionsTreeWithSplitter">
|
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
||||||
{/* Collections Tree Expanded - Start */}
|
<div className="collectionsTreeWithSplitter">
|
||||||
<ResourceTreeContainer
|
{/* Collections Tree Expanded - Start */}
|
||||||
container={explorer}
|
<ResourceTreeContainer
|
||||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
container={explorer}
|
||||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||||
/>
|
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||||
{/* Collections Tree Expanded - End */}
|
/>
|
||||||
{/* Collections Tree Collapsed - Start */}
|
{/* Collections Tree Expanded - End */}
|
||||||
<CollapsedResourceTree
|
{/* Collections Tree Collapsed - Start */}
|
||||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
<CollapsedResourceTree
|
||||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||||
/>
|
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||||
{/* Collections Tree Collapsed - End */}
|
/>
|
||||||
|
{/* Collections Tree Collapsed - End */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
<Tabs explorer={explorer} />
|
||||||
<Tabs explorer={explorer} />
|
</div>
|
||||||
</div>
|
{/* Collections Tree and Tabs - End */}
|
||||||
{/* Collections Tree and Tabs - End */}
|
<div
|
||||||
<div
|
className="dataExplorerErrorConsoleContainer"
|
||||||
className="dataExplorerErrorConsoleContainer"
|
role="contentinfo"
|
||||||
role="contentinfo"
|
aria-label="Notification console"
|
||||||
aria-label="Notification console"
|
id="explorerNotificationConsole"
|
||||||
id="explorerNotificationConsole"
|
>
|
||||||
>
|
<NotificationConsole />
|
||||||
<NotificationConsole />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<SidePanel />
|
||||||
|
<Dialog />
|
||||||
|
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||||
|
{<SQLQuickstartTutorial />}
|
||||||
|
{<MongoQuickstartTutorial />}
|
||||||
|
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||||
</div>
|
</div>
|
||||||
<SidePanel />
|
</KeyboardShortcutRoot>
|
||||||
<Dialog />
|
|
||||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
|
||||||
{<SQLQuickstartTutorial />}
|
|
||||||
{<MongoQuickstartTutorial />}
|
|
||||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue