diff --git a/static/app/components/commandPalette/__stories__/components.tsx b/static/app/components/commandPalette/__stories__/components.tsx
index 5e51e673193fa6..ea0b08af142a3e 100644
--- a/static/app/components/commandPalette/__stories__/components.tsx
+++ b/static/app/components/commandPalette/__stories__/components.tsx
@@ -2,7 +2,7 @@ import {useCallback} from 'react';
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
import {CommandPaletteProvider} from 'sentry/components/commandPalette/ui/cmdk';
-import {CMDKAction, CMDKGroup} from 'sentry/components/commandPalette/ui/cmdk';
+import {CMDKAction} from 'sentry/components/commandPalette/ui/cmdk';
import type {CMDKActionData} from 'sentry/components/commandPalette/ui/cmdk';
import type {CollectionTreeNode} from 'sentry/components/commandPalette/ui/collection';
import {CommandPalette} from 'sentry/components/commandPalette/ui/commandPalette';
@@ -30,14 +30,14 @@ export function CommandPaletteDemo() {
display={{label: 'Execute an action'}}
onAction={() => addSuccessMessage('Action executed')}
/>
-
+
addSuccessMessage('Child action executed')}
/>
-
+
-
+
addSuccessMessage('Select all')}
@@ -46,7 +46,7 @@ export function CommandPaletteDemo() {
display={{label: 'Deselect all'}}
onAction={() => addSuccessMessage('Deselect all')}
/>
-
+
);
diff --git a/static/app/components/commandPalette/ui/cmdk.tsx b/static/app/components/commandPalette/ui/cmdk.tsx
index 6c365edeb387fd..5847fb97e04242 100644
--- a/static/app/components/commandPalette/ui/cmdk.tsx
+++ b/static/app/components/commandPalette/ui/cmdk.tsx
@@ -26,7 +26,7 @@ interface CMDKActionDataBase {
}
interface CMDKActionDataTo extends CMDKActionDataBase {
- to: string;
+ to: LocationDescriptor;
}
interface CMDKActionDataOnAction extends CMDKActionDataBase {
@@ -50,7 +50,7 @@ export const CMDKCollection = makeCollection();
/**
* Root provider for the command palette. Wrap the component tree that
- * contains CMDKGroup/CMDKAction registrations and the CommandPalette UI.
+ * contains CMDKAction registrations and the CommandPalette UI.
*/
export function CommandPaletteProvider({children}: {children: React.ReactNode}) {
return (
@@ -62,25 +62,39 @@ export function CommandPaletteProvider({children}: {children: React.ReactNode})
);
}
-interface CMDKGroupProps {
+interface CMDKActionProps {
display: DisplayProps;
children?: React.ReactNode | ((data: CommandPaletteAsyncResult[]) => React.ReactNode);
keywords?: string[];
+ onAction?: () => void;
resource?: (query: string) => CMDKQueryOptions;
+ to?: LocationDescriptor;
}
-type CMDKActionProps =
- | {display: DisplayProps; to: LocationDescriptor; keywords?: string[]}
- | {display: DisplayProps; onAction: () => void; keywords?: string[]};
-
/**
- * Registers a node in the collection and propagates its key to children via
- * GroupContext. When a `resource` prop is provided, fetches data using the
- * current query and passes results to a render-prop children function.
+ * Registers a node in the collection. A node becomes a group when it has
+ * children — they register under this node as their parent. Provide `to` for
+ * navigation, `onAction` for a callback, or `resource` with a render-prop
+ * children function to fetch and populate async results.
*/
-export function CMDKGroup({display, keywords, resource, children}: CMDKGroupProps) {
+export function CMDKAction({
+ display,
+ keywords,
+ children,
+ to,
+ onAction,
+ resource,
+}: CMDKActionProps) {
const ref = CommandPaletteSlot.useSlotOutletRef();
- const key = CMDKCollection.useRegisterNode({display, keywords, resource, ref});
+
+ const nodeData: CMDKActionData =
+ to === undefined
+ ? onAction === undefined
+ ? {display, keywords, ref, resource}
+ : {display, keywords, ref, onAction}
+ : {display, keywords, ref, to};
+
+ const key = CMDKCollection.useRegisterNode(nodeData);
const {query} = useCommandPaletteState();
const resourceOptions = resource
@@ -91,6 +105,10 @@ export function CMDKGroup({display, keywords, resource, children}: CMDKGroupProp
enabled: !!resource && (resourceOptions.enabled ?? true),
});
+ if (!children) {
+ return null;
+ }
+
const resolvedChildren =
typeof children === 'function' ? (data ? children(data) : null) : children;
@@ -100,12 +118,3 @@ export function CMDKGroup({display, keywords, resource, children}: CMDKGroupProp
);
}
-
-/**
- * Registers a leaf action node in the collection.
- */
-export function CMDKAction(props: CMDKActionProps) {
- const ref = CommandPaletteSlot.useSlotOutletRef();
- CMDKCollection.useRegisterNode({...props, ref});
- return null;
-}
diff --git a/static/app/components/commandPalette/ui/commandPalette.spec.tsx b/static/app/components/commandPalette/ui/commandPalette.spec.tsx
index 7f5176bb06b112..f31510c64c8aee 100644
--- a/static/app/components/commandPalette/ui/commandPalette.spec.tsx
+++ b/static/app/components/commandPalette/ui/commandPalette.spec.tsx
@@ -26,7 +26,7 @@ import {closeModal} from 'sentry/actionCreators/modal';
import * as modalActions from 'sentry/actionCreators/modal';
import type {CommandPaletteAction} from 'sentry/components/commandPalette/types';
import {CommandPaletteProvider} from 'sentry/components/commandPalette/ui/cmdk';
-import {CMDKAction, CMDKGroup} from 'sentry/components/commandPalette/ui/cmdk';
+import {CMDKAction} from 'sentry/components/commandPalette/ui/cmdk';
import type {CMDKActionData} from 'sentry/components/commandPalette/ui/cmdk';
import type {CollectionTreeNode} from 'sentry/components/commandPalette/ui/collection';
import {CommandPalette} from 'sentry/components/commandPalette/ui/commandPalette';
@@ -43,9 +43,9 @@ function ActionsToJSX({actions}: {actions: CommandPaletteAction[]}) {
{actions.map((action, i) => {
if ('actions' in action) {
return (
-
+
-
+
);
}
if ('to' in action) {
diff --git a/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx b/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx
index 436c89e0458551..78a26d1d0f4456 100644
--- a/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx
+++ b/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx
@@ -46,7 +46,7 @@ import {ISSUE_TAXONOMY_CONFIG} from 'sentry/views/issueList/taxonomies';
import {useStarredIssueViews} from 'sentry/views/navigation/secondary/sections/issues/issueViews/useStarredIssueViews';
import {getUserOrgNavigationConfiguration} from 'sentry/views/settings/organization/userOrgNavigationConfiguration';
-import {CMDKAction, CMDKGroup} from './cmdk';
+import {CMDKAction} from './cmdk';
import {CommandPaletteSlot} from './commandPaletteSlot';
const DSN_ICONS: React.ReactElement[] = [
@@ -82,8 +82,8 @@ export function GlobalCommandPaletteActions() {
return (
-
- }}>
+
+ }}>
{Object.values(ISSUE_TAXONOMY_CONFIG).map(config => (
))}
-
+
- }}>
+ }}>
{organization.features.includes('ourlogs-enabled') && (
@@ -135,14 +135,14 @@ export function GlobalCommandPaletteActions() {
display={{label: t('All Queries')}}
to={`${prefix}/explore/saved-queries/`}
/>
-
+
- }}>
+ }}>
- }}>
+ }}>
{starredDashboards.map(dashboard => (
))}
-
-
+
+
{organization.features.includes('performance-view') && (
- }}>
+ }}>
-
+
)}
- }}>
+ }}>
{getUserOrgNavigationConfiguration().flatMap(section =>
section.items.map(item => (
))
)}
-
+
- }}>
+ }}>
{projects.map(project => (
))}
-
-
+
+
-
+
}}
keywords={[t('add dashboard')]}
@@ -232,10 +232,10 @@ export function GlobalCommandPaletteActions() {
keywords={[t('team invite')]}
onAction={openInviteMembersModal}
/>
-
+
-
-
+ }}
keywords={[t('client keys'), t('dsn keys')]}
>
@@ -250,9 +250,9 @@ export function GlobalCommandPaletteActions() {
to={`/settings/${organization.slug}/projects/${project.slug}/keys/`}
/>
))}
-
+
{hasDsnLookup && (
-
data.map((item, i) => renderAsyncResult(item, i))
}
-
+
)}
-
+
-
+
}}
onAction={() => window.open('https://docs.sentry.io', '_blank', 'noreferrer')}
@@ -314,7 +314,7 @@ export function GlobalCommandPaletteActions() {
window.open('https://sentry.io/changelog/', '_blank', 'noreferrer')
}
/>
- {
return queryOptions({
@@ -353,11 +353,11 @@ export function GlobalCommandPaletteActions() {
{(data: CommandPaletteAsyncResult[]) =>
data.map((item, i) => renderAsyncResult(item, i))
}
-
-
+
+
-
- }}>
+
+ }}>
{
@@ -382,8 +382,8 @@ export function GlobalCommandPaletteActions() {
addSuccessMessage(t('Theme preference saved: Dark'));
}}
/>
-
-
+
+
);
}
diff --git a/static/app/components/commandPalette/useCommandPaletteActions.mdx b/static/app/components/commandPalette/useCommandPaletteActions.mdx
index 8dd1761aee6d01..5f5b11de3840a9 100644
--- a/static/app/components/commandPalette/useCommandPaletteActions.mdx
+++ b/static/app/components/commandPalette/useCommandPaletteActions.mdx
@@ -18,14 +18,14 @@ import {CommandPaletteDemo} from './__stories__/components';
## Basic Usage
-Use `CMDKAction` and `CMDKGroup` JSX components inside your page or feature component to register contextual actions with the global command palette. Actions are registered on mount and automatically unregistered on unmount, so they only appear in the palette while your component is rendered. This is ideal for page‑specific shortcuts.
+Use `CMDKAction` JSX components inside your page or feature component to register contextual actions with the global command palette. Actions are registered on mount and automatically unregistered on unmount, so they only appear in the palette while your component is rendered. This is ideal for page‑specific shortcuts.
Wrap your tree in `CommandPaletteProvider` and place the `CommandPalette` UI component wherever you want the dialog to render. Then declare actions anywhere inside the provider:
- **Navigation actions**: Provide a `to` prop to navigate when selected.
- **Callback actions**: Provide an `onAction` handler to execute when selected.
-- **Grouped actions**: Wrap `CMDKAction` children inside a `CMDKGroup` to show a second level. Selecting the parent reveals its children.
-- **Async actions**: Provide a `resource` prop to a `CMDKGroup` and use the render-prop children signature to generate actions from fetched data.
+- **Grouped actions**: Nest `CMDKAction` children inside another `CMDKAction` to show a second level. Selecting the parent reveals its children.
+- **Async actions**: Provide a `resource` prop and use the render-prop children signature to fetch and populate async results.
@@ -39,7 +39,7 @@ import {useCallback} from 'react';
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
import {
CMDKAction,
- CMDKGroup,
+ CMDKAction,
CommandPaletteProvider,
} from 'sentry/components/commandPalette/ui/cmdk';
import type {CMDKActionData} from 'sentry/components/commandPalette/ui/cmdk';
@@ -74,16 +74,16 @@ function YourComponent() {
/>
{/* Grouped actions */}
-
+
addSuccessMessage('Child action executed')}
/>
-
+
{/* The command palette UI — also accepts inline actions via children */}
-
+
addSuccessMessage('Select all')}
@@ -92,7 +92,7 @@ function YourComponent() {
display={{label: 'Deselect all'}}
onAction={() => addSuccessMessage('Deselect all')}
/>
-
+
);
@@ -101,12 +101,12 @@ function YourComponent() {
## Async / Resource Actions
-`CMDKGroup` accepts a `resource` prop — a function that takes the current search query and returns a TanStack Query options object. The `children` prop becomes a render function that receives the fetched results:
+`CMDKAction` accepts a `resource` prop — a function that takes the current search query and returns a TanStack Query options object. The `children` prop becomes a render function that receives the fetched results:
```tsx
import type {CommandPaletteAsyncResult} from 'sentry/components/commandPalette/types';
- ({
queryKey: ['/projects/', {query}],
@@ -121,5 +121,5 @@ import type {CommandPaletteAsyncResult} from 'sentry/components/commandPalette/t
))
}
-;
+;
```