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 ( + + data={{ recipient: "", message: "" }} + onSubmit={data => { + command.execute(data); + onClose(); + }} + > + {({ submit }) => ( +
+ + + + + + +
+ +
+
+ )} + + ); +}; 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 👇 */}