Skip to content

Commit c85059a

Browse files
JonasBaclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 4406dc2 commit c85059a

File tree

5 files changed

+82
-73
lines changed

5 files changed

+82
-73
lines changed

static/app/components/commandPalette/__stories__/components.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {useCallback} from 'react';
22

33
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
44
import {CommandPaletteProvider} from 'sentry/components/commandPalette/ui/cmdk';
5-
import {CMDKAction, CMDKGroup} from 'sentry/components/commandPalette/ui/cmdk';
5+
import {CMDKAction} from 'sentry/components/commandPalette/ui/cmdk';
66
import type {CMDKActionData} from 'sentry/components/commandPalette/ui/cmdk';
77
import type {CollectionTreeNode} from 'sentry/components/commandPalette/ui/collection';
88
import {CommandPalette} from 'sentry/components/commandPalette/ui/commandPalette';
@@ -30,14 +30,14 @@ export function CommandPaletteDemo() {
3030
display={{label: 'Execute an action'}}
3131
onAction={() => addSuccessMessage('Action executed')}
3232
/>
33-
<CMDKGroup display={{label: 'Parent action'}}>
33+
<CMDKAction display={{label: 'Parent action'}}>
3434
<CMDKAction
3535
display={{label: 'Child action'}}
3636
onAction={() => addSuccessMessage('Child action executed')}
3737
/>
38-
</CMDKGroup>
38+
</CMDKAction>
3939
<CommandPalette onAction={handleAction}>
40-
<CMDKGroup display={{label: 'Issues List'}}>
40+
<CMDKAction display={{label: 'Issues List'}}>
4141
<CMDKAction
4242
display={{label: 'Select all'}}
4343
onAction={() => addSuccessMessage('Select all')}
@@ -46,7 +46,7 @@ export function CommandPaletteDemo() {
4646
display={{label: 'Deselect all'}}
4747
onAction={() => addSuccessMessage('Deselect all')}
4848
/>
49-
</CMDKGroup>
49+
</CMDKAction>
5050
</CommandPalette>
5151
</CommandPaletteProvider>
5252
);

static/app/components/commandPalette/ui/cmdk.tsx

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface CMDKActionDataBase {
2626
}
2727

2828
interface CMDKActionDataTo extends CMDKActionDataBase {
29-
to: string;
29+
to: LocationDescriptor;
3030
}
3131

3232
interface CMDKActionDataOnAction extends CMDKActionDataBase {
@@ -50,7 +50,7 @@ export const CMDKCollection = makeCollection<CMDKActionData>();
5050

5151
/**
5252
* Root provider for the command palette. Wrap the component tree that
53-
* contains CMDKGroup/CMDKAction registrations and the CommandPalette UI.
53+
* contains CMDKAction registrations and the CommandPalette UI.
5454
*/
5555
export function CommandPaletteProvider({children}: {children: React.ReactNode}) {
5656
return (
@@ -62,25 +62,39 @@ export function CommandPaletteProvider({children}: {children: React.ReactNode})
6262
);
6363
}
6464

65-
interface CMDKGroupProps {
65+
interface CMDKActionProps {
6666
display: DisplayProps;
6767
children?: React.ReactNode | ((data: CommandPaletteAsyncResult[]) => React.ReactNode);
6868
keywords?: string[];
69+
onAction?: () => void;
6970
resource?: (query: string) => CMDKQueryOptions;
71+
to?: LocationDescriptor;
7072
}
7173

72-
type CMDKActionProps =
73-
| {display: DisplayProps; to: LocationDescriptor; keywords?: string[]}
74-
| {display: DisplayProps; onAction: () => void; keywords?: string[]};
75-
7674
/**
77-
* Registers a node in the collection and propagates its key to children via
78-
* GroupContext. When a `resource` prop is provided, fetches data using the
79-
* current query and passes results to a render-prop children function.
75+
* Registers a node in the collection. A node becomes a group when it has
76+
* children — they register under this node as their parent. Provide `to` for
77+
* navigation, `onAction` for a callback, or `resource` with a render-prop
78+
* children function to fetch and populate async results.
8079
*/
81-
export function CMDKGroup({display, keywords, resource, children}: CMDKGroupProps) {
80+
export function CMDKAction({
81+
display,
82+
keywords,
83+
children,
84+
to,
85+
onAction,
86+
resource,
87+
}: CMDKActionProps) {
8288
const ref = CommandPaletteSlot.useSlotOutletRef();
83-
const key = CMDKCollection.useRegisterNode({display, keywords, resource, ref});
89+
90+
const nodeData: CMDKActionData =
91+
to === undefined
92+
? onAction === undefined
93+
? {display, keywords, ref, resource}
94+
: {display, keywords, ref, onAction}
95+
: {display, keywords, ref, to};
96+
97+
const key = CMDKCollection.useRegisterNode(nodeData);
8498
const {query} = useCommandPaletteState();
8599

86100
const resourceOptions = resource
@@ -91,6 +105,10 @@ export function CMDKGroup({display, keywords, resource, children}: CMDKGroupProp
91105
enabled: !!resource && (resourceOptions.enabled ?? true),
92106
});
93107

108+
if (!children) {
109+
return null;
110+
}
111+
94112
const resolvedChildren =
95113
typeof children === 'function' ? (data ? children(data) : null) : children;
96114

@@ -100,12 +118,3 @@ export function CMDKGroup({display, keywords, resource, children}: CMDKGroupProp
100118
</CMDKCollection.Context.Provider>
101119
);
102120
}
103-
104-
/**
105-
* Registers a leaf action node in the collection.
106-
*/
107-
export function CMDKAction(props: CMDKActionProps) {
108-
const ref = CommandPaletteSlot.useSlotOutletRef();
109-
CMDKCollection.useRegisterNode({...props, ref});
110-
return null;
111-
}

static/app/components/commandPalette/ui/commandPalette.spec.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {closeModal} from 'sentry/actionCreators/modal';
2626
import * as modalActions from 'sentry/actionCreators/modal';
2727
import type {CommandPaletteAction} from 'sentry/components/commandPalette/types';
2828
import {CommandPaletteProvider} from 'sentry/components/commandPalette/ui/cmdk';
29-
import {CMDKAction, CMDKGroup} from 'sentry/components/commandPalette/ui/cmdk';
29+
import {CMDKAction} from 'sentry/components/commandPalette/ui/cmdk';
3030
import type {CMDKActionData} from 'sentry/components/commandPalette/ui/cmdk';
3131
import type {CollectionTreeNode} from 'sentry/components/commandPalette/ui/collection';
3232
import {CommandPalette} from 'sentry/components/commandPalette/ui/commandPalette';
@@ -43,9 +43,9 @@ function ActionsToJSX({actions}: {actions: CommandPaletteAction[]}) {
4343
{actions.map((action, i) => {
4444
if ('actions' in action) {
4545
return (
46-
<CMDKGroup key={i} display={action.display} keywords={action.keywords}>
46+
<CMDKAction key={i} display={action.display} keywords={action.keywords}>
4747
<ActionsToJSX actions={action.actions} />
48-
</CMDKGroup>
48+
</CMDKAction>
4949
);
5050
}
5151
if ('to' in action) {

static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import {ISSUE_TAXONOMY_CONFIG} from 'sentry/views/issueList/taxonomies';
4646
import {useStarredIssueViews} from 'sentry/views/navigation/secondary/sections/issues/issueViews/useStarredIssueViews';
4747
import {getUserOrgNavigationConfiguration} from 'sentry/views/settings/organization/userOrgNavigationConfiguration';
4848

49-
import {CMDKAction, CMDKGroup} from './cmdk';
49+
import {CMDKAction} from './cmdk';
5050
import {CommandPaletteSlot} from './commandPaletteSlot';
5151

5252
const DSN_ICONS: React.ReactElement[] = [
@@ -82,8 +82,8 @@ export function GlobalCommandPaletteActions() {
8282

8383
return (
8484
<CommandPaletteSlot name="global">
85-
<CMDKGroup display={{label: t('Go to...')}}>
86-
<CMDKGroup display={{label: t('Issues'), icon: <IconIssues />}}>
85+
<CMDKAction display={{label: t('Go to...')}}>
86+
<CMDKAction display={{label: t('Issues'), icon: <IconIssues />}}>
8787
<CMDKAction display={{label: t('Feed')}} to={`${prefix}/issues/`} />
8888
{Object.values(ISSUE_TAXONOMY_CONFIG).map(config => (
8989
<CMDKAction
@@ -104,9 +104,9 @@ export function GlobalCommandPaletteActions() {
104104
to={`${prefix}/issues/views/${starredView.id}/`}
105105
/>
106106
))}
107-
</CMDKGroup>
107+
</CMDKAction>
108108

109-
<CMDKGroup display={{label: t('Explore'), icon: <IconCompass />}}>
109+
<CMDKAction display={{label: t('Explore'), icon: <IconCompass />}}>
110110
<CMDKAction display={{label: t('Traces')}} to={`${prefix}/explore/traces/`} />
111111
{organization.features.includes('ourlogs-enabled') && (
112112
<CMDKAction display={{label: t('Logs')}} to={`${prefix}/explore/logs/`} />
@@ -135,26 +135,26 @@ export function GlobalCommandPaletteActions() {
135135
display={{label: t('All Queries')}}
136136
to={`${prefix}/explore/saved-queries/`}
137137
/>
138-
</CMDKGroup>
138+
</CMDKAction>
139139

140-
<CMDKGroup display={{label: t('Dashboards'), icon: <IconDashboard />}}>
140+
<CMDKAction display={{label: t('Dashboards'), icon: <IconDashboard />}}>
141141
<CMDKAction
142142
display={{label: t('All Dashboards')}}
143143
to={`${prefix}/dashboards/`}
144144
/>
145-
<CMDKGroup display={{label: t('Starred Dashboards'), icon: <IconStar />}}>
145+
<CMDKAction display={{label: t('Starred Dashboards'), icon: <IconStar />}}>
146146
{starredDashboards.map(dashboard => (
147147
<CMDKAction
148148
key={dashboard.id}
149149
display={{label: dashboard.title, icon: <IconStar />}}
150150
to={`${prefix}/dashboard/${dashboard.id}/`}
151151
/>
152152
))}
153-
</CMDKGroup>
154-
</CMDKGroup>
153+
</CMDKAction>
154+
</CMDKAction>
155155

156156
{organization.features.includes('performance-view') && (
157-
<CMDKGroup display={{label: t('Insights'), icon: <IconGraph type="area" />}}>
157+
<CMDKAction display={{label: t('Insights'), icon: <IconGraph type="area" />}}>
158158
<CMDKAction
159159
display={{label: t('Frontend')}}
160160
to={`${prefix}/insights/${FRONTEND_LANDING_SUB_PATH}/`}
@@ -186,18 +186,18 @@ export function GlobalCommandPaletteActions() {
186186
display={{label: t('All Projects')}}
187187
to={`${prefix}/insights/projects/`}
188188
/>
189-
</CMDKGroup>
189+
</CMDKAction>
190190
)}
191191

192-
<CMDKGroup display={{label: t('Settings'), icon: <IconSettings />}}>
192+
<CMDKAction display={{label: t('Settings'), icon: <IconSettings />}}>
193193
{getUserOrgNavigationConfiguration().flatMap(section =>
194194
section.items.map(item => (
195195
<CMDKAction key={item.path} display={{label: item.title}} to={item.path} />
196196
))
197197
)}
198-
</CMDKGroup>
198+
</CMDKAction>
199199

200-
<CMDKGroup display={{label: t('Project Settings'), icon: <IconSettings />}}>
200+
<CMDKAction display={{label: t('Project Settings'), icon: <IconSettings />}}>
201201
{projects.map(project => (
202202
<CMDKAction
203203
key={project.id}
@@ -208,10 +208,10 @@ export function GlobalCommandPaletteActions() {
208208
to={`/settings/${organization.slug}/projects/${project.slug}/`}
209209
/>
210210
))}
211-
</CMDKGroup>
212-
</CMDKGroup>
211+
</CMDKAction>
212+
</CMDKAction>
213213

214-
<CMDKGroup display={{label: t('Add')}}>
214+
<CMDKAction display={{label: t('Add')}}>
215215
<CMDKAction
216216
display={{label: t('Create Dashboard'), icon: <IconAdd />}}
217217
keywords={[t('add dashboard')]}
@@ -232,10 +232,10 @@ export function GlobalCommandPaletteActions() {
232232
keywords={[t('team invite')]}
233233
onAction={openInviteMembersModal}
234234
/>
235-
</CMDKGroup>
235+
</CMDKAction>
236236

237-
<CMDKGroup display={{label: t('DSN')}} keywords={[t('client keys')]}>
238-
<CMDKGroup
237+
<CMDKAction display={{label: t('DSN')}} keywords={[t('client keys')]}>
238+
<CMDKAction
239239
display={{label: t('Project DSN Keys'), icon: <IconLock locked />}}
240240
keywords={[t('client keys'), t('dsn keys')]}
241241
>
@@ -250,9 +250,9 @@ export function GlobalCommandPaletteActions() {
250250
to={`/settings/${organization.slug}/projects/${project.slug}/keys/`}
251251
/>
252252
))}
253-
</CMDKGroup>
253+
</CMDKAction>
254254
{hasDsnLookup && (
255-
<CMDKGroup
255+
<CMDKAction
256256
display={{
257257
label: t('Reverse DSN lookup'),
258258
details: t(
@@ -287,11 +287,11 @@ export function GlobalCommandPaletteActions() {
287287
{(data: CommandPaletteAsyncResult[]) =>
288288
data.map((item, i) => renderAsyncResult(item, i))
289289
}
290-
</CMDKGroup>
290+
</CMDKAction>
291291
)}
292-
</CMDKGroup>
292+
</CMDKAction>
293293

294-
<CMDKGroup display={{label: t('Help')}}>
294+
<CMDKAction display={{label: t('Help')}}>
295295
<CMDKAction
296296
display={{label: t('Open Documentation'), icon: <IconDocs />}}
297297
onAction={() => window.open('https://docs.sentry.io', '_blank', 'noreferrer')}
@@ -314,7 +314,7 @@ export function GlobalCommandPaletteActions() {
314314
window.open('https://sentry.io/changelog/', '_blank', 'noreferrer')
315315
}
316316
/>
317-
<CMDKGroup
317+
<CMDKAction
318318
display={{label: t('Search Results')}}
319319
resource={(query: string): CMDKQueryOptions => {
320320
return queryOptions({
@@ -353,11 +353,11 @@ export function GlobalCommandPaletteActions() {
353353
{(data: CommandPaletteAsyncResult[]) =>
354354
data.map((item, i) => renderAsyncResult(item, i))
355355
}
356-
</CMDKGroup>
357-
</CMDKGroup>
356+
</CMDKAction>
357+
</CMDKAction>
358358

359-
<CMDKGroup display={{label: t('Interface')}}>
360-
<CMDKGroup display={{label: t('Change Color Theme'), icon: <IconSettings />}}>
359+
<CMDKAction display={{label: t('Interface')}}>
360+
<CMDKAction display={{label: t('Change Color Theme'), icon: <IconSettings />}}>
361361
<CMDKAction
362362
display={{label: t('System')}}
363363
onAction={async () => {
@@ -382,8 +382,8 @@ export function GlobalCommandPaletteActions() {
382382
addSuccessMessage(t('Theme preference saved: Dark'));
383383
}}
384384
/>
385-
</CMDKGroup>
386-
</CMDKGroup>
385+
</CMDKAction>
386+
</CMDKAction>
387387
</CommandPaletteSlot>
388388
);
389389
}

0 commit comments

Comments
 (0)