diff --git a/extensions/commandPalette/CommandPalette.tsx b/extensions/commandPalette/CommandPalette.tsx
new file mode 100644
index 00000000000..bc090b506dc
--- /dev/null
+++ b/extensions/commandPalette/CommandPalette.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+import { Admin } from "webiny/extensions";
+
+export const CommandPalette = () => {
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/extensions/commandPalette/commands/formCommand/FormCommand.tsx b/extensions/commandPalette/commands/formCommand/FormCommand.tsx
new file mode 100644
index 00000000000..112c190dc2a
--- /dev/null
+++ b/extensions/commandPalette/commands/formCommand/FormCommand.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { Command, RegisterFeature, createFeature } from "webiny/admin";
+import { SendMessageDetailView } from "./SendMessageDetailView.js";
+
+interface SendMessageParams {
+ recipient: string;
+ message: string;
+}
+
+class SendMessageCommand implements Command.Interface {
+ name = "send-message";
+ label = "Send Message";
+ description = "Send a message to someone";
+ category = "Demo";
+ keywords = ["send", "message", "form"];
+ shortcut = "cmd+shift+m";
+ detailView = SendMessageDetailView;
+
+ execute(params?: unknown) {
+ const { recipient, message } = params as SendMessageParams;
+ alert(`Message sent to ${recipient}: "${message}"`);
+ }
+}
+
+const SendMessageCommandImpl = Command.createImplementation({
+ implementation: SendMessageCommand,
+ dependencies: []
+});
+
+const SendMessageCommandFeature = createFeature({
+ name: "SendMessageCommand",
+ register(container) {
+ container.register(SendMessageCommandImpl);
+ }
+});
+
+export default () => {
+ return ;
+};
diff --git a/extensions/commandPalette/commands/formCommand/SendMessageDetailView.tsx b/extensions/commandPalette/commands/formCommand/SendMessageDetailView.tsx
new file mode 100644
index 00000000000..cbf66b9c4ea
--- /dev/null
+++ b/extensions/commandPalette/commands/formCommand/SendMessageDetailView.tsx
@@ -0,0 +1,41 @@
+import React from "react";
+import { Form, Bind } from "webiny/admin/form";
+import { Input, Button } from "webiny/admin/ui";
+import { Command } from "webiny/admin";
+
+interface FormData {
+ recipient: string;
+ message: string;
+}
+
+export const SendMessageDetailView = ({ command, onClose }: Command.DetailProps) => {
+ return (
+
+ );
+};
diff --git a/extensions/commandPalette/commands/simpleCommand/SimpleCommand.tsx b/extensions/commandPalette/commands/simpleCommand/SimpleCommand.tsx
new file mode 100644
index 00000000000..db9383c7285
--- /dev/null
+++ b/extensions/commandPalette/commands/simpleCommand/SimpleCommand.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+import { Command, RegisterFeature, createFeature } from "webiny/admin";
+
+class SayHelloCommand implements Command.Interface {
+ name = "say-hello";
+ label = "Say Hello";
+ description = "Displays a greeting in the console";
+ category = "Demo";
+ keywords = ["hello", "greet", "demo"];
+
+ execute() {
+ alert("Hello from the Command Palette!");
+ }
+}
+
+const SayHelloCommandImpl = Command.createImplementation({
+ implementation: SayHelloCommand,
+ dependencies: []
+});
+
+const SayHelloCommandFeature = createFeature({
+ name: "SayHelloCommand",
+ register(container) {
+ container.register(SayHelloCommandImpl);
+ }
+});
+
+export default () => {
+ return ;
+};
diff --git a/packages/admin-ui/src/CommandPalette/CommandPalette.tsx b/packages/admin-ui/src/CommandPalette/CommandPalette.tsx
new file mode 100644
index 00000000000..580be3ed652
--- /dev/null
+++ b/packages/admin-ui/src/CommandPalette/CommandPalette.tsx
@@ -0,0 +1,78 @@
+import * as React from "react";
+import { Command as CommandPrimitive } from "cmdk";
+import { ReactComponent as ArrowLeftIcon } from "@webiny/icons/arrow_back.svg";
+import { makeDecoratable, cn, withStaticProps } from "~/utils.js";
+import type { CommandPaletteProps } from "./types.js";
+import { CommandPaletteContent } from "./components/CommandPaletteContent.js";
+import { CommandPaletteSearch } from "./components/CommandPaletteSearch.js";
+import { CommandPaletteList } from "./components/CommandPaletteList.js";
+
+const CommandPaletteBase = ({
+ open,
+ onOpenChange,
+ commands,
+ detailView,
+ onSelectCommand,
+ onCancelCommand,
+ placeholder
+}: CommandPaletteProps) => {
+ const handleOpenChange = React.useCallback(
+ (nextOpen: boolean) => {
+ if (!nextOpen) {
+ onCancelCommand();
+ }
+ onOpenChange(nextOpen);
+ },
+ [onOpenChange, onCancelCommand]
+ );
+
+ return (
+
+ {detailView ? (
+
+
+
+
+ {detailView.icon && (
+
+ {detailView.icon}
+
+ )}
+
+ {detailView.label}
+
+
+
+ {detailView.element}
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+};
+
+CommandPaletteBase.displayName = "CommandPalette";
+
+const DecoratableCommandPalette = makeDecoratable("CommandPalette", CommandPaletteBase);
+
+const CommandPalette = withStaticProps(DecoratableCommandPalette, {});
+
+export { CommandPalette, type CommandPaletteProps };
diff --git a/packages/admin-ui/src/CommandPalette/components/CommandPaletteContent.tsx b/packages/admin-ui/src/CommandPalette/components/CommandPaletteContent.tsx
new file mode 100644
index 00000000000..bfc319d9a65
--- /dev/null
+++ b/packages/admin-ui/src/CommandPalette/components/CommandPaletteContent.tsx
@@ -0,0 +1,47 @@
+import * as React from "react";
+import { Dialog as DialogPrimitive, VisuallyHidden } from "radix-ui";
+import { cn } from "~/utils.js";
+
+interface CommandPaletteContentProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ children: React.ReactNode;
+}
+
+const CommandPaletteContent = ({ open, onOpenChange, children }: CommandPaletteContentProps) => {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export { CommandPaletteContent };
diff --git a/packages/admin-ui/src/CommandPalette/components/CommandPaletteItem.tsx b/packages/admin-ui/src/CommandPalette/components/CommandPaletteItem.tsx
new file mode 100644
index 00000000000..279c647c092
--- /dev/null
+++ b/packages/admin-ui/src/CommandPalette/components/CommandPaletteItem.tsx
@@ -0,0 +1,51 @@
+import * as React from "react";
+import { Command as CommandPrimitive } from "cmdk";
+import { cn } from "~/utils.js";
+import type { CommandPaletteCommand } from "../types.js";
+
+interface CommandPaletteItemProps {
+ command: CommandPaletteCommand;
+ onSelect: () => void;
+}
+
+const CommandPaletteItem = ({ command, onSelect }: CommandPaletteItemProps) => {
+ return (
+
+ {command.icon && (
+
+ {command.icon}
+
+ )}
+
+ {command.label}
+ {command.description && (
+
+ {command.description}
+
+ )}
+
+ {command.shortcut && (
+
+ {command.shortcut}
+
+ )}
+
+ );
+};
+
+export { CommandPaletteItem };
diff --git a/packages/admin-ui/src/CommandPalette/components/CommandPaletteList.tsx b/packages/admin-ui/src/CommandPalette/components/CommandPaletteList.tsx
new file mode 100644
index 00000000000..012a1a51b72
--- /dev/null
+++ b/packages/admin-ui/src/CommandPalette/components/CommandPaletteList.tsx
@@ -0,0 +1,69 @@
+import * as React from "react";
+import { Command as CommandPrimitive } from "cmdk";
+import { cn } from "~/utils.js";
+import type { CommandPaletteCommand } from "../types.js";
+import { CommandPaletteItem } from "./CommandPaletteItem.js";
+
+interface CommandPaletteListProps {
+ commands: CommandPaletteCommand[];
+ onSelect: (name: string) => void;
+}
+
+const CommandPaletteList = ({ commands, onSelect }: CommandPaletteListProps) => {
+ const grouped = React.useMemo(() => {
+ const groups = new Map();
+ for (const cmd of commands) {
+ const category = cmd.category ?? "";
+ const list = groups.get(category) ?? [];
+ list.push(cmd);
+ groups.set(category, list);
+ }
+ return groups;
+ }, [commands]);
+
+ return (
+
+
+ No commands found.
+
+ {[...grouped.entries()].map(([category, cmds]) => {
+ if (category === "") {
+ return cmds.map(cmd => (
+ onSelect(cmd.name)}
+ />
+ ));
+ }
+ return (
+
+ {cmds.map(cmd => (
+ onSelect(cmd.name)}
+ />
+ ))}
+
+ );
+ })}
+
+ );
+};
+
+export { CommandPaletteList };
diff --git a/packages/admin-ui/src/CommandPalette/components/CommandPaletteSearch.tsx b/packages/admin-ui/src/CommandPalette/components/CommandPaletteSearch.tsx
new file mode 100644
index 00000000000..2ee6bf27e53
--- /dev/null
+++ b/packages/admin-ui/src/CommandPalette/components/CommandPaletteSearch.tsx
@@ -0,0 +1,31 @@
+import * as React from "react";
+import { Command as CommandPrimitive } from "cmdk";
+import { cn } from "~/utils.js";
+
+interface CommandPaletteSearchProps {
+ placeholder?: string;
+ value?: string;
+ onValueChange?: (value: string) => void;
+}
+
+const CommandPaletteSearch = ({
+ placeholder = "Search commands…",
+ ...props
+}: CommandPaletteSearchProps) => {
+ return (
+
+
+
+ );
+};
+
+export { CommandPaletteSearch };
diff --git a/packages/admin-ui/src/CommandPalette/components/index.ts b/packages/admin-ui/src/CommandPalette/components/index.ts
new file mode 100644
index 00000000000..86b2b0adc27
--- /dev/null
+++ b/packages/admin-ui/src/CommandPalette/components/index.ts
@@ -0,0 +1,4 @@
+export { CommandPaletteContent } from "./CommandPaletteContent.js";
+export { CommandPaletteSearch } from "./CommandPaletteSearch.js";
+export { CommandPaletteList } from "./CommandPaletteList.js";
+export { CommandPaletteItem } from "./CommandPaletteItem.js";
diff --git a/packages/admin-ui/src/CommandPalette/index.ts b/packages/admin-ui/src/CommandPalette/index.ts
new file mode 100644
index 00000000000..a792bab9e15
--- /dev/null
+++ b/packages/admin-ui/src/CommandPalette/index.ts
@@ -0,0 +1,2 @@
+export { CommandPalette, type CommandPaletteProps } from "./CommandPalette.js";
+export type { CommandPaletteCommand } from "./types.js";
diff --git a/packages/admin-ui/src/CommandPalette/types.ts b/packages/admin-ui/src/CommandPalette/types.ts
new file mode 100644
index 00000000000..5c69beeadce
--- /dev/null
+++ b/packages/admin-ui/src/CommandPalette/types.ts
@@ -0,0 +1,28 @@
+import type React from "react";
+
+export interface CommandPaletteCommand {
+ name: string;
+ label: string;
+ description?: string;
+ icon?: React.ReactNode;
+ category?: string;
+ keywords?: string[];
+ shortcut?: string;
+ hasDetailView: boolean;
+}
+
+export interface CommandPaletteDetailView {
+ label: string;
+ icon?: React.ReactNode;
+ element: React.ReactNode;
+}
+
+export interface CommandPaletteProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ commands: CommandPaletteCommand[];
+ detailView?: CommandPaletteDetailView;
+ onSelectCommand: (name: string) => void;
+ onCancelCommand: () => void;
+ placeholder?: string;
+}
diff --git a/packages/admin-ui/src/index.ts b/packages/admin-ui/src/index.ts
index 7aaaa3f6e36..494f30ffc02 100644
--- a/packages/admin-ui/src/index.ts
+++ b/packages/admin-ui/src/index.ts
@@ -8,6 +8,7 @@ export * from "./Checkbox/index.js";
export * from "./CheckboxGroup/index.js";
export * from "./CodeEditor/index.js";
export * from "./ColorPicker/index.js";
+export * from "./CommandPalette/index.js";
export * from "./DataList/index.js";
export * from "./DataTable/index.js";
export * from "./DelayedOnChange/index.js";
diff --git a/packages/app-admin/src/base/Base.tsx b/packages/app-admin/src/base/Base.tsx
index 75c06a60bf2..32cada22280 100644
--- a/packages/app-admin/src/base/Base.tsx
+++ b/packages/app-admin/src/base/Base.tsx
@@ -4,6 +4,7 @@ import { RoutesConfig } from "./Base/RoutesConfig.js";
import { Tenant } from "./Base/Tenant.js";
import { UserMenu } from "./Base/UserMenu.js";
import { LexicalPreset } from "./Base/LexicalPreset.js";
+import { CommandPaletteExtension } from "~/presentation/commandPalette/CommandPaletteExtension.js";
const BaseExtension = () => {
return (
@@ -13,6 +14,7 @@ const BaseExtension = () => {
+
>
);
};
diff --git a/packages/app-admin/src/exports/admin.ts b/packages/app-admin/src/exports/admin.ts
index 255ff3df5f4..5f4e7be0ea8 100644
--- a/packages/app-admin/src/exports/admin.ts
+++ b/packages/app-admin/src/exports/admin.ts
@@ -1,3 +1,4 @@
+export { Command } from "~/presentation/commandPalette/index.js";
export { DevToolsSection } from "~/components/index.js";
export { createPermissionSchema } from "~/permissions/index.js";
export { createHasPermission } from "~/permissions/index.js";
diff --git a/packages/app-admin/src/hooks/useHotkeys.ts b/packages/app-admin/src/hooks/useHotkeys.ts
index 64f0a017a0e..bb3239dba24 100644
--- a/packages/app-admin/src/hooks/useHotkeys.ts
+++ b/packages/app-admin/src/hooks/useHotkeys.ts
@@ -88,21 +88,24 @@ export function useHotkeys(props: HookProps) {
const prevPropsRef = useRef();
const firstRenderRef = useRef(true);
- useEffect(function () {
- if (firstRenderRef.current || prevPropsRef.current?.disabled !== disabled) {
- firstRenderRef.current = false;
- if (disabled) {
- unregisterZIndex(props);
- } else {
- registerZIndex(props);
+ useEffect(
+ function () {
+ if (firstRenderRef.current || prevPropsRef.current?.disabled !== disabled) {
+ firstRenderRef.current = false;
+ if (disabled) {
+ unregisterZIndex(props);
+ } else {
+ registerZIndex(props);
+ }
}
- }
- if (!disabled && typeof keys === "object") {
- Object.assign(state.handlers[zIndex], keys);
- }
- prevPropsRef.current = { ...props };
- });
+ if (!disabled && typeof keys === "object") {
+ Object.assign(state.handlers[zIndex], keys);
+ }
+ prevPropsRef.current = { ...props };
+ },
+ [keys]
+ );
useEffect(function () {
return function () {
diff --git a/packages/app-admin/src/index.ts b/packages/app-admin/src/index.ts
index 574e7d12c34..45e5645eb7b 100644
--- a/packages/app-admin/src/index.ts
+++ b/packages/app-admin/src/index.ts
@@ -47,6 +47,9 @@ export type { AaclPermission } from "./features/wcp/types.js";
export type { Tenant } from "./features/tenancy/types.js";
export { BuildParamsFeature } from "./features/buildParams/feature.js";
+export { CommandPaletteFeature } from "./presentation/commandPalette/feature.js";
+export { Command } from "./presentation/commandPalette/abstractions.js";
+export type { ICommand, CommandDetailProps } from "./presentation/commandPalette/abstractions.js";
// Hooks
export * from "./hooks/index.js";
diff --git a/packages/app-admin/src/presentation/commandPalette/CommandPalette.tsx b/packages/app-admin/src/presentation/commandPalette/CommandPalette.tsx
new file mode 100644
index 00000000000..2efaa734a2d
--- /dev/null
+++ b/packages/app-admin/src/presentation/commandPalette/CommandPalette.tsx
@@ -0,0 +1,74 @@
+import React, { useCallback, useEffect, useMemo } from "react";
+import { observer } from "mobx-react-lite";
+import { useFeature } from "@webiny/app";
+import { CommandPalette as CommandPaletteUi } from "@webiny/admin-ui";
+import { useHotkeys } from "~/hooks/useHotkeys.js";
+import { CommandPaletteFeature } from "~/presentation/commandPalette/feature.js";
+
+export const CommandPalette = observer(() => {
+ const { presenter } = useFeature(CommandPaletteFeature);
+ const { vm } = presenter;
+
+ useEffect(() => {
+ presenter.init();
+ }, [presenter]);
+
+ const keys = useMemo(() => {
+ return {
+ "mod+k": (e: KeyboardEvent) => {
+ e.preventDefault();
+ presenter.toggle();
+ },
+ backspace: (e: KeyboardEvent) => {
+ if (e.target instanceof HTMLInputElement) {
+ return;
+ }
+ e.preventDefault();
+ presenter.cancelCommand();
+ },
+ ...presenter.shortcutKeys
+ };
+ }, [presenter, presenter.shortcutKeys]);
+
+ useHotkeys({
+ zIndex: 100,
+ keys
+ });
+
+ const handleOpenChange = useCallback(
+ (nextOpen: boolean) => {
+ if (nextOpen) {
+ presenter.open();
+ } else {
+ presenter.close();
+ }
+ },
+ [presenter]
+ );
+
+ const activeCommand = vm.activeCommand;
+ const detailView = activeCommand
+ ? {
+ label: activeCommand.command.label,
+ icon: activeCommand.command.icon,
+ element: (
+ presenter.close()}
+ onBack={() => presenter.cancelCommand()}
+ />
+ )
+ }
+ : undefined;
+
+ return (
+ presenter.useCommand(name)}
+ onCancelCommand={() => presenter.cancelCommand()}
+ />
+ );
+});
diff --git a/packages/app-admin/src/presentation/commandPalette/CommandPaletteExtension.tsx b/packages/app-admin/src/presentation/commandPalette/CommandPaletteExtension.tsx
new file mode 100644
index 00000000000..7c4360e570d
--- /dev/null
+++ b/packages/app-admin/src/presentation/commandPalette/CommandPaletteExtension.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Plugins } from "@webiny/app";
+import { CommandPalette } from "./CommandPalette.js";
+import { RegisterFeature } from "~/components/index.js";
+import { CommandPaletteFeature } from "~/presentation/commandPalette/feature.js";
+import { DeveloperMode } from "~/components/index.js";
+
+export const CommandPaletteExtension = () => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/app-admin/src/presentation/commandPalette/CommandPalettePresenter.ts b/packages/app-admin/src/presentation/commandPalette/CommandPalettePresenter.ts
new file mode 100644
index 00000000000..a86aac37481
--- /dev/null
+++ b/packages/app-admin/src/presentation/commandPalette/CommandPalettePresenter.ts
@@ -0,0 +1,99 @@
+import { makeAutoObservable } from "mobx";
+import {
+ Command,
+ CommandPalettePresenter as Abstraction,
+ type CommandPaletteViewModel
+} from "./abstractions.js";
+
+export class CommandPalettePresenter implements Abstraction.Interface {
+ private isOpen = false;
+ private activeCommandName: string | null = null;
+ private resolvedCommands: Command.Interface[] = [];
+
+ constructor(private getCommands: () => Command.Interface[]) {
+ makeAutoObservable(this);
+ }
+
+ init(): void {
+ this.resolvedCommands = this.getCommands();
+ }
+
+ get shortcutKeys(): Record void> {
+ const keys: Record void> = {};
+ for (const cmd of this.resolvedCommands) {
+ if (cmd.shortcut) {
+ keys[cmd.shortcut] = (e: KeyboardEvent) => {
+ e.preventDefault();
+ this.useCommand(cmd.name);
+ };
+ }
+ }
+ return keys;
+ }
+
+ get vm(): CommandPaletteViewModel {
+ const activeCmd =
+ this.activeCommandName !== null
+ ? this.resolvedCommands.find(c => c.name === this.activeCommandName)
+ : null;
+
+ return {
+ isOpen: this.isOpen,
+ commands: this.resolvedCommands.map(cmd => ({
+ name: cmd.name,
+ label: cmd.label,
+ description: cmd.description,
+ icon: cmd.icon,
+ category: cmd.category,
+ keywords: cmd.keywords,
+ shortcut: cmd.shortcut,
+ hasDetailView: Boolean(cmd.detailView)
+ })),
+ activeCommand:
+ activeCmd && activeCmd.detailView
+ ? {
+ command: activeCmd,
+ DetailView: activeCmd.detailView
+ }
+ : null
+ };
+ }
+
+ open(): void {
+ this.resolvedCommands = this.getCommands();
+ this.activeCommandName = null;
+ this.isOpen = true;
+ }
+
+ close(): void {
+ this.isOpen = false;
+ this.activeCommandName = null;
+ }
+
+ toggle(): void {
+ if (this.isOpen) {
+ this.close();
+ } else {
+ this.open();
+ }
+ }
+
+ useCommand(name: string): void {
+ const cmd = this.resolvedCommands.find(c => c.name === name);
+ if (!cmd) {
+ return;
+ }
+
+ if (cmd.detailView) {
+ this.activeCommandName = name;
+ this.isOpen = true;
+ } else {
+ cmd.execute();
+ this.close();
+ }
+ }
+
+ cancelCommand(): void {
+ this.activeCommandName = null;
+ }
+}
diff --git a/packages/app-admin/src/presentation/commandPalette/abstractions.ts b/packages/app-admin/src/presentation/commandPalette/abstractions.ts
new file mode 100644
index 00000000000..ab37fecdc7f
--- /dev/null
+++ b/packages/app-admin/src/presentation/commandPalette/abstractions.ts
@@ -0,0 +1,70 @@
+import type React from "react";
+import { createAbstraction } from "@webiny/feature/admin";
+import { Abstraction } from "@webiny/di";
+
+export interface CommandDetailProps {
+ command: ICommand;
+ onClose: () => void;
+ onBack: () => void;
+}
+
+export interface ICommand {
+ name: string;
+ label: string;
+ description?: string;
+ icon?: React.ReactNode;
+ category?: string;
+ keywords?: string[];
+ shortcut?: string;
+ execute(params?: unknown): void | Promise;
+ detailView?: React.ComponentType;
+}
+
+export const Command = createAbstraction("Command");
+
+export namespace Command {
+ export type Interface = ICommand;
+ export type DetailProps = CommandDetailProps;
+}
+
+export interface CommandItemVm {
+ name: string;
+ label: string;
+ description?: string;
+ icon?: React.ReactNode;
+ category?: string;
+ keywords?: string[];
+ shortcut?: string;
+ hasDetailView: boolean;
+}
+
+export interface ActiveCommandVm {
+ command: ICommand;
+ DetailView: React.ComponentType;
+}
+
+export interface CommandPaletteViewModel {
+ isOpen: boolean;
+ commands: CommandItemVm[];
+ activeCommand: ActiveCommandVm | null;
+}
+
+export interface ICommandPalettePresenter {
+ vm: CommandPaletteViewModel;
+ shortcutKeys: Record void>;
+ init(): void;
+ open(): void;
+ close(): void;
+ toggle(): void;
+ useCommand(name: string): void;
+ cancelCommand(): void;
+}
+
+export const CommandPalettePresenter = new Abstraction(
+ "CommandPalettePresenter"
+);
+
+export namespace CommandPalettePresenter {
+ export type Interface = ICommandPalettePresenter;
+ export type ViewModel = CommandPaletteViewModel;
+}
diff --git a/packages/app-admin/src/presentation/commandPalette/feature.ts b/packages/app-admin/src/presentation/commandPalette/feature.ts
new file mode 100644
index 00000000000..506ffd544b7
--- /dev/null
+++ b/packages/app-admin/src/presentation/commandPalette/feature.ts
@@ -0,0 +1,21 @@
+import { createFeature } from "@webiny/feature/admin";
+import { Container } from "@webiny/di";
+import { CommandPalettePresenter as Abstraction } from "./abstractions.js";
+import { CommandPalettePresenter } from "./CommandPalettePresenter.js";
+import { Command } from "./abstractions.js";
+
+export const CommandPaletteFeature = createFeature({
+ name: "CommandPalette",
+ register(container: Container) {
+ container.registerFactory(Abstraction, () => {
+ return new CommandPalettePresenter(() => {
+ return container.resolveAll(Command);
+ });
+ });
+ },
+ resolve(container: Container) {
+ return {
+ presenter: container.resolve(Abstraction)
+ };
+ }
+});
diff --git a/packages/app-admin/src/presentation/commandPalette/index.ts b/packages/app-admin/src/presentation/commandPalette/index.ts
new file mode 100644
index 00000000000..fd1233a3b7d
--- /dev/null
+++ b/packages/app-admin/src/presentation/commandPalette/index.ts
@@ -0,0 +1,3 @@
+export { Command, CommandPalettePresenter } from "./abstractions.js";
+export type { ICommand, CommandDetailProps, CommandPaletteViewModel } from "./abstractions.js";
+export { CommandPaletteFeature } from "./feature.js";
diff --git a/packages/webiny/src/admin.ts b/packages/webiny/src/admin.ts
index 35926687620..24f5b12abea 100644
--- a/packages/webiny/src/admin.ts
+++ b/packages/webiny/src/admin.ts
@@ -4,6 +4,7 @@ export { createProviderPlugin } from "@webiny/app/core/createProviderPlugin.js";
export { createProvider } from "@webiny/app/core/createProvider.js";
export { Provider } from "@webiny/app/core/Provider.js";
export { Plugin } from "@webiny/app/core/Plugin.js";
+export { Command } from "@webiny/app-admin/presentation/commandPalette/index.js";
export { DevToolsSection } from "@webiny/app-admin/components/index.js";
export { createPermissionSchema } from "@webiny/app-admin/permissions/index.js";
export { createHasPermission } from "@webiny/app-admin/permissions/index.js";
diff --git a/webiny.config.tsx b/webiny.config.tsx
index 878ff310ae5..3f7b7e63d50 100644
--- a/webiny.config.tsx
+++ b/webiny.config.tsx
@@ -2,6 +2,7 @@ import React from "react";
import { Admin, Api, Cli, Infra, Project } from "webiny/extensions";
import { Cognito } from "@webiny/cognito";
import { MyFeature } from "@/extensions/myFeature/Extension.js";
+import { CommandPalette } from "@/extensions/commandPalette/CommandPalette.js";
// import { MyIdpExtension } from "./extensions/idp/okta/MyIdpExtension.js";
export const Extensions = () => {
@@ -14,6 +15,7 @@ export const Extensions = () => {
{/**/}
+
{/* Infra 👇 */}