a system of contributors?
This commit is contained in:
parent
e441b75325
commit
2089d8ca9e
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* React component for Command button component.
|
||||
*/
|
||||
import { KeyboardShortcutAction } from "KeyboardShortcuts";
|
||||
import { ExtendedKeyboardEvent } from "mousetrap";
|
||||
import * as React from "react";
|
||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
|
@ -30,7 +32,7 @@ export interface CommandButtonComponentProps {
|
|||
/**
|
||||
* Click handler for command button click
|
||||
*/
|
||||
onCommandClick: (e: React.SyntheticEvent) => void;
|
||||
onCommandClick: (e: React.SyntheticEvent | ExtendedKeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Label for the button
|
||||
|
@ -107,10 +109,13 @@ export interface CommandButtonComponentProps {
|
|||
* Vertical bar to divide buttons
|
||||
*/
|
||||
isDivider?: boolean;
|
||||
|
||||
/**
|
||||
* Aria-label for the button
|
||||
*/
|
||||
ariaLabel: string;
|
||||
|
||||
keyboardShortcut?: KeyboardShortcutAction;
|
||||
}
|
||||
|
||||
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { KeyboardShortcutAction, KeyboardShortcutContributor, KeyboardShortcutHandler, useKeyboardShortcutContributor } from "KeyboardShortcuts";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
|
@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||
const buttons = useCommandBar((state) => state.contextButtons);
|
||||
const isHidden = useCommandBar((state) => state.isHidden);
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
const setKeyboardShortcutHandlers = useKeyboardShortcutContributor(KeyboardShortcutContributor.COMMAND_BAR);
|
||||
|
||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||
const buttons =
|
||||
|
@ -105,6 +107,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||
},
|
||||
};
|
||||
|
||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||
const handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>> = {};
|
||||
allButtons.forEach((button) => {
|
||||
if(button.keyboardShortcut) {
|
||||
handlers[button.keyboardShortcut] = (e) => {
|
||||
button.onCommandClick(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
setKeyboardShortcutHandlers(handlers);
|
||||
|
||||
return (
|
||||
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||
<FluentCommandBar
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { KeyboardShortcutAction } from "KeyboardShortcuts";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import * as React from "react";
|
||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||
|
@ -297,6 +298,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
|||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: KeyboardShortcutAction.NEW_QUERY,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||
|
@ -312,6 +314,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
|||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: KeyboardShortcutAction.NEW_QUERY,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
|
|||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardShortcutAction } from "KeyboardShortcuts";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
|
@ -393,6 +394,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: KeyboardShortcutAction.EXECUTE_ITEM,
|
||||
onCommandClick: this.props.isSampleCopilotActive
|
||||
? () => OnExecuteQueryClick(this.props.copilotStore)
|
||||
: this.onExecuteQueryClick,
|
||||
|
|
|
@ -1,115 +1,135 @@
|
|||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { userContext } from "UserContext";
|
||||
import Mousetrap, { ExtendedKeyboardEvent } from "mousetrap";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import create, { UseStore } from "zustand";
|
||||
|
||||
type KeyboardShortcutRootProps = React.PropsWithChildren<unknown>;
|
||||
type KeyboardShortcutHandler = (e: ExtendedKeyboardEvent, combo: string) => boolean | void;
|
||||
// Provides a system of Keyboard Shortcuts that can be contributed to by different parts of the application.
|
||||
//
|
||||
// The goals of this system are:
|
||||
// * Shortcuts can be contributed from different parts of the application (e.g. the command bar, specified editor tabs, etc.)
|
||||
// * Contributors may only provide some of their shortcuts, others may be out-of-scope for the current context.
|
||||
// * Contributors don't have to add/remove handlers individually, they can use a declarative pattern to set all their handlers at once.
|
||||
//
|
||||
// So, in order to do that, we store handlers in a two-level hierarchy:
|
||||
// 1. We store a mapping of contributors to their Contributions.
|
||||
// 2. Each Contribution is a mapping of actions to their handlers.
|
||||
//
|
||||
// Thus, a Contributor sets all its handlers at once, replacing all handlers previously contributed by that Contributor.
|
||||
// The system then merges all Contributions into a single set of handlers, with duplicate handlers being handled in the order that the Contributors are processed.
|
||||
|
||||
export interface KeyboardShortcutBinding {
|
||||
/**
|
||||
* The keyboard shortcut to bind to. This can be a single string or an array of strings.
|
||||
* Any combination supported by Mousetrap (https://craig.is/killing/mice#api.bind) is valid here.
|
||||
*/
|
||||
keys: string | string[],
|
||||
|
||||
/**
|
||||
* The handler to run when the keyboard shortcut is pressed.
|
||||
* @param e The keyboard event that triggered the shortcut.
|
||||
* @param combo The specific keyboard combination that was matched (in case a single handler is used for multiple shortcuts).
|
||||
* @returns If the handler returns `false`, the default action for the keyboard shortcut will be prevented AND propagation of the event will be stopped.
|
||||
*/
|
||||
handler: KeyboardShortcutHandler,
|
||||
export type KeyboardShortcutHandler = (e: ExtendedKeyboardEvent, combo: string) => boolean | void;
|
||||
|
||||
/**
|
||||
* The event to bind the keyboard shortcut to (keydown, keyup, etc.).
|
||||
* The default is 'keydown'
|
||||
*/
|
||||
action?: string,
|
||||
/**
|
||||
* Lists all the possible contributors to keyboard shortcut handlers.
|
||||
*
|
||||
* A "Contributor" is a part of the application that can contribute keyboard shortcut handlers.
|
||||
* The contributor must set all it's keyboard shortcut handlers at once.
|
||||
* This allows the contributor to easily replace all it's keyboard shortcuts at once.
|
||||
*
|
||||
* For example, the command bar adds/removes keyboard shortcut handlers based on the current context, using the existing logic that determines which buttons are shown.
|
||||
* Isolating contributors like this allow the command bar to easily replace all it's keyboard shortcuts when the context changes without breaking keyboard shortcuts contributed by other parts of the application.
|
||||
*/
|
||||
export enum KeyboardShortcutContributor {
|
||||
COMMAND_BAR = "COMMAND_BAR",
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the provided keyboard shortcut handler in one that only runs if a collection is selected.
|
||||
* @param callback The callback to run if a collection is selected.
|
||||
* @returns If the handler returns `false`, the default action for the keyboard shortcut will be prevented AND propagation of the event will be stopped.
|
||||
* The order in which contributors are processed.
|
||||
* This is important because the last contributor to set a handler for a given action will be the one that is used.
|
||||
*/
|
||||
function withSelectedCollection(callback: (selectedCollection: ViewModels.Collection, e: ExtendedKeyboardEvent, combo: string) => boolean | void): KeyboardShortcutHandler {
|
||||
return (e, combo) => {
|
||||
const state = useSelectedNode.getState();
|
||||
if (!state.selectedNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCollection = state.findSelectedCollection();
|
||||
if (selectedCollection) {
|
||||
return callback(selectedCollection, e, combo);
|
||||
}
|
||||
};
|
||||
const contributorOrder: KeyboardShortcutContributor[] = [
|
||||
KeyboardShortcutContributor.COMMAND_BAR,
|
||||
];
|
||||
|
||||
/**
|
||||
* The possible actions that can be triggered by keyboard shortcuts.
|
||||
*/
|
||||
export enum KeyboardShortcutAction {
|
||||
NEW_QUERY = "NEW_QUERY",
|
||||
EXECUTE_ITEM = "EXECUTE_ITEM",
|
||||
}
|
||||
|
||||
const bindings: KeyboardShortcutBinding[] = [
|
||||
{
|
||||
keys: ["ctrl+j"],
|
||||
handler: withSelectedCollection((selectedCollection) => {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
selectedCollection.onNewQueryClick(selectedCollection);
|
||||
return false;
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
},
|
||||
{
|
||||
keys: ["shift+enter"],
|
||||
handler: () => {
|
||||
alert("TODO: Execute Item");
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ["esc"],
|
||||
handler: () => {
|
||||
alert("TODO: Cancel Query");
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ["mod+s"],
|
||||
handler: () => {
|
||||
alert("TODO: Save Query");
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ["mod+o"],
|
||||
handler: () => {
|
||||
alert("TODO: Open Query");
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ["mod+shift+o"],
|
||||
handler: () => {
|
||||
alert("TODO: Open Query from Disk");
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ["mod+s"],
|
||||
handler: () => {
|
||||
alert("TODO: Save");
|
||||
return false;
|
||||
},
|
||||
},
|
||||
]
|
||||
/**
|
||||
* The default 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 though, it will not be triggered unless a handler is set for it.
|
||||
*/
|
||||
const bindings: Record<KeyboardShortcutAction, string[]> = {
|
||||
[KeyboardShortcutAction.NEW_QUERY]: ["ctrl+j"],
|
||||
[KeyboardShortcutAction.EXECUTE_ITEM]: ["shift+enter"],
|
||||
};
|
||||
|
||||
export function KeyboardShortcutRoot({ children }: KeyboardShortcutRootProps) {
|
||||
/**
|
||||
* Represents all the handlers provided by a contributor.
|
||||
*/
|
||||
export type KeyboardShortcutContribution = Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>;
|
||||
|
||||
interface KeyboardShortcutState {
|
||||
/**
|
||||
* Collects all the contributions from different contributors.
|
||||
*/
|
||||
contributions: Partial<Record<KeyboardShortcutContributor, KeyboardShortcutContribution>>;
|
||||
|
||||
/**
|
||||
* A merged set of all the handlers from all contributors.
|
||||
*/
|
||||
allHandlers: KeyboardShortcutContribution;
|
||||
|
||||
/**
|
||||
* Sets the keyboard shortcut handlers for a given contributor.
|
||||
*/
|
||||
setHandlers: (contributor: KeyboardShortcutContributor, handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the setHandlers function for a given contributor.
|
||||
* @param contributor The contributor to get the setHandlers function for.
|
||||
* @returns A function that sets the keyboard shortcut handlers for the given contributor.
|
||||
*/
|
||||
export const useKeyboardShortcutContributor = (contributor: KeyboardShortcutContributor) => {
|
||||
const setHandlers = useKeyboardShortcutHandlers.getState().setHandlers;
|
||||
return (handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => {
|
||||
setHandlers(contributor, handlers);
|
||||
};
|
||||
}
|
||||
|
||||
const useKeyboardShortcutHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
|
||||
contributions: {},
|
||||
allHandlers: {},
|
||||
setHandlers: (contributor: KeyboardShortcutContributor, handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => {
|
||||
const current = get();
|
||||
|
||||
// Update the list of contributions.
|
||||
const newContributions = { ...current.contributions, [contributor]: handlers };
|
||||
|
||||
// Merge all the contributions into a single set of handlers.
|
||||
const allHandlers: KeyboardShortcutContribution = {};
|
||||
contributorOrder.forEach((contributor) => {
|
||||
const contribution = newContributions[contributor];
|
||||
if (contribution) {
|
||||
(Object.keys(contribution) as KeyboardShortcutAction[]).forEach((action) => {
|
||||
allHandlers[action] = contribution[action];
|
||||
});
|
||||
}
|
||||
});
|
||||
set({ contributions: newContributions, allHandlers })
|
||||
}
|
||||
}));
|
||||
|
||||
function createHandler(action: KeyboardShortcutAction): KeyboardShortcutHandler {
|
||||
return (e, combo) => {
|
||||
const handlers = useKeyboardShortcutHandlers.getState().allHandlers;
|
||||
const handler = handlers[action];
|
||||
if (handler) {
|
||||
return handler(e, combo);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function KeyboardShortcutRoot(props: React.HTMLProps<HTMLDivElement>) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
React.useEffect(() => {
|
||||
const m = new Mousetrap(document.body);
|
||||
const m = new Mousetrap(ref.current);
|
||||
const existingStopCallback = m.stopCallback;
|
||||
m.stopCallback = (e, element, combo) => {
|
||||
// Don't block mousetrap callback in the Monaco editor.
|
||||
|
@ -120,12 +140,11 @@ export function KeyboardShortcutRoot({ children }: KeyboardShortcutRootProps) {
|
|||
return existingStopCallback(e, element, combo);
|
||||
};
|
||||
|
||||
bindings.forEach(b => {
|
||||
m.bind(b.keys, b.handler, b.action);
|
||||
(Object.keys(bindings) as KeyboardShortcutAction[]).forEach((action) => {
|
||||
m.bind(bindings[action], createHandler(action));
|
||||
});
|
||||
}, []); // Using an empty dependency array means React will only run this _once_ when the component is mounted.
|
||||
}, [ref]); // We only need to re-render the component when the ref changes.
|
||||
|
||||
return <>
|
||||
{children}
|
||||
</>;
|
||||
return <div ref={ref} {...props}>
|
||||
</div>;
|
||||
}
|
86
src/Main.tsx
86
src/Main.tsx
|
@ -92,53 +92,51 @@ const App: React.FunctionComponent = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<KeyboardShortcutRoot>
|
||||
<div className="flexContainer" aria-hidden="false">
|
||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||
<div id="freeTierTeachingBubble"> </div>
|
||||
{/* Main Command Bar - Start */}
|
||||
<CommandBar container={explorer} />
|
||||
{/* Collections Tree and Tabs - Begin */}
|
||||
<div className="resourceTreeAndTabs">
|
||||
{/* Collections Tree - Start */}
|
||||
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
|
||||
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
||||
<div className="collectionsTreeWithSplitter">
|
||||
{/* Collections Tree Expanded - Start */}
|
||||
<ResourceTreeContainer
|
||||
container={explorer}
|
||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||
/>
|
||||
{/* Collections Tree Expanded - End */}
|
||||
{/* Collections Tree Collapsed - Start */}
|
||||
<CollapsedResourceTree
|
||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||
/>
|
||||
{/* Collections Tree Collapsed - End */}
|
||||
</div>
|
||||
<KeyboardShortcutRoot className="flexContainer" aria-hidden="false">
|
||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||
<div id="freeTierTeachingBubble"> </div>
|
||||
{/* Main Command Bar - Start */}
|
||||
<CommandBar container={explorer} />
|
||||
{/* Collections Tree and Tabs - Begin */}
|
||||
<div className="resourceTreeAndTabs">
|
||||
{/* Collections Tree - Start */}
|
||||
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
|
||||
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
||||
<div className="collectionsTreeWithSplitter">
|
||||
{/* Collections Tree Expanded - Start */}
|
||||
<ResourceTreeContainer
|
||||
container={explorer}
|
||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||
/>
|
||||
{/* Collections Tree Expanded - End */}
|
||||
{/* Collections Tree Collapsed - Start */}
|
||||
<CollapsedResourceTree
|
||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||
/>
|
||||
{/* Collections Tree Collapsed - End */}
|
||||
</div>
|
||||
)}
|
||||
<Tabs explorer={explorer} />
|
||||
</div>
|
||||
{/* Collections Tree and Tabs - End */}
|
||||
<div
|
||||
className="dataExplorerErrorConsoleContainer"
|
||||
role="contentinfo"
|
||||
aria-label="Notification console"
|
||||
id="explorerNotificationConsole"
|
||||
>
|
||||
<NotificationConsole />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Tabs explorer={explorer} />
|
||||
</div>
|
||||
{/* Collections Tree and Tabs - End */}
|
||||
<div
|
||||
className="dataExplorerErrorConsoleContainer"
|
||||
role="contentinfo"
|
||||
aria-label="Notification console"
|
||||
id="explorerNotificationConsole"
|
||||
>
|
||||
<NotificationConsole />
|
||||
</div>
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
{<SQLQuickstartTutorial />}
|
||||
{<MongoQuickstartTutorial />}
|
||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||
</div>
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
{<SQLQuickstartTutorial />}
|
||||
{<MongoQuickstartTutorial />}
|
||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||
</KeyboardShortcutRoot>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue