From c85059aeb9b9cc30a8f32d0ad277d1e9ac88fed3 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 9 Apr 2026 04:27:37 +0200 Subject: [PATCH] ref(cmdk): Merge CMDKGroup and CMDKAction into single CMDKAction component A node becomes a group by virtue of having children registered under it, so the Group/Action split was an artificial distinction. A single CMDKAction now covers all cases: navigation (to), callbacks (onAction), async resource groups (resource + render-prop children), and plain parent groups (children only). Co-Authored-By: Claude Sonnet 4 --- .../commandPalette/__stories__/components.tsx | 10 +-- .../app/components/commandPalette/ui/cmdk.tsx | 51 ++++++++------ .../commandPalette/ui/commandPalette.spec.tsx | 6 +- .../ui/commandPaletteGlobalActions.tsx | 66 +++++++++---------- .../useCommandPaletteActions.mdx | 22 +++---- 5 files changed, 82 insertions(+), 73 deletions(-) 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 )) } -; +; ```