Skip to content

Commit 36fed98

Browse files
JonasBacodex
andcommitted
feat(cmdk): Group project settings actions
Group the project settings command palette entries under a single parent so browse mode stays more compact and easier to scan. Add section-specific icons for the main project settings groups, keep legacy integrations as a direct action, and cover the new structure in the existing command palette test. Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 2118410 commit 36fed98

File tree

2 files changed

+142
-49
lines changed

2 files changed

+142
-49
lines changed

static/app/views/settings/project/projectSettingsCommandPaletteActions.spec.tsx

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
import {PluginFixture} from 'sentry-fixture/plugin';
23
import {ProjectFixture} from 'sentry-fixture/project';
34

45
import {getProjectSettingsCommandPaletteSections} from 'sentry/views/settings/project/projectSettingsCommandPaletteActions';
@@ -13,8 +14,13 @@ describe('ProjectSettingsCommandPaletteActions', () => {
1314
const project = ProjectFixture({
1415
slug: 'frontend',
1516
plugins: [
16-
{enabled: true, id: 'github', isDeprecated: false, name: 'GitHub'},
17-
{enabled: true, id: 'legacy', isDeprecated: true, name: 'Legacy Plugin'},
17+
PluginFixture({enabled: true, id: 'github', isDeprecated: false, name: 'GitHub'}),
18+
PluginFixture({
19+
enabled: true,
20+
id: 'legacy',
21+
isDeprecated: true,
22+
name: 'Legacy Plugin',
23+
}),
1824
],
1925
});
2026

@@ -23,29 +29,32 @@ describe('ProjectSettingsCommandPaletteActions', () => {
2329
expect(sections).toEqual(
2430
expect.arrayContaining([
2531
expect.objectContaining({
26-
label: 'Project',
32+
label: 'Project Settings',
2733
items: expect.arrayContaining([
2834
expect.objectContaining({
29-
display: expect.objectContaining({label: 'General Settings'}),
30-
to: '/settings/acme/projects/frontend/',
35+
label: 'General',
36+
items: expect.arrayContaining([
37+
expect.objectContaining({
38+
display: expect.objectContaining({label: 'General Settings'}),
39+
to: '/settings/acme/projects/frontend/',
40+
}),
41+
]),
3142
}),
32-
]),
33-
}),
34-
expect.objectContaining({
35-
label: 'Processing',
36-
items: expect.arrayContaining([
3743
expect.objectContaining({
38-
display: expect.objectContaining({label: 'Performance'}),
39-
to: '/settings/acme/projects/frontend/performance/',
44+
label: 'Processing',
45+
items: expect.arrayContaining([
46+
expect.objectContaining({
47+
display: expect.objectContaining({label: 'Performance'}),
48+
to: '/settings/acme/projects/frontend/performance/',
49+
}),
50+
]),
51+
}),
52+
expect.objectContaining({
53+
label: 'SDK setup',
4054
}),
41-
]),
42-
}),
43-
expect.objectContaining({
44-
label: 'Legacy Integrations',
45-
items: expect.arrayContaining([
4655
expect.objectContaining({
47-
display: expect.objectContaining({label: 'GitHub'}),
48-
to: '/settings/acme/projects/frontend/plugins/github/',
56+
display: expect.objectContaining({label: 'Legacy Integrations'}),
57+
to: '/settings/acme/projects/frontend/plugins/',
4958
}),
5059
]),
5160
}),
@@ -55,8 +64,16 @@ describe('ProjectSettingsCommandPaletteActions', () => {
5564
expect(sections).not.toEqual(
5665
expect.arrayContaining([
5766
expect.objectContaining({
67+
label: 'Project Settings',
5868
items: expect.arrayContaining([
59-
expect.objectContaining({to: '/settings/acme/projects/frontend/replays/'}),
69+
expect.objectContaining({
70+
label: 'Processing',
71+
items: expect.arrayContaining([
72+
expect.objectContaining({
73+
to: '/settings/acme/projects/frontend/replays/',
74+
}),
75+
]),
76+
}),
6077
]),
6178
}),
6279
expect.objectContaining({

static/app/views/settings/project/projectSettingsCommandPaletteActions.tsx

Lines changed: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,37 @@ import {ProjectAvatar} from '@sentry/scraps/avatar';
55

66
import {CMDKAction} from 'sentry/components/commandPalette/ui/cmdk';
77
import {CommandPaletteSlot} from 'sentry/components/commandPalette/ui/commandPaletteSlot';
8+
import {IconCode, IconProject, IconStack} from 'sentry/icons';
89
import type {Organization} from 'sentry/types/organization';
910
import type {Project} from 'sentry/types/project';
1011
import {replaceRouterParams} from 'sentry/utils/replaceRouterParams';
1112
import {getNavigationConfiguration} from 'sentry/views/settings/project/navigationConfiguration';
1213
import type {NavigationGroupProps, NavigationItem} from 'sentry/views/settings/types';
1314

14-
type ProjectSettingsCommandPaletteAction = {
15+
type ProjectSettingsCommandPaletteEntry = {
1516
display: {
1617
label: string;
1718
};
1819
keywords: string[];
1920
to: string;
2021
};
2122

23+
type ProjectSettingsCommandPaletteGroup = {
24+
items: Array<{
25+
display: {
26+
label: string;
27+
};
28+
keywords: string[];
29+
to: string;
30+
}>;
31+
label: string;
32+
icon?: ReactNode;
33+
};
34+
2235
type ProjectSettingsCommandPaletteSection = {
23-
icon: ReactNode;
24-
items: ProjectSettingsCommandPaletteAction[];
36+
items: Array<ProjectSettingsCommandPaletteEntry | ProjectSettingsCommandPaletteGroup>;
2537
label: string;
38+
icon?: ReactNode;
2639
};
2740

2841
function shouldShowItem(
@@ -50,29 +63,77 @@ export function getProjectSettingsCommandPaletteSections({
5063
organization,
5164
project,
5265
};
66+
const groupedSectionLabels = new Set([
67+
'General',
68+
'Processing',
69+
'SDK setup',
70+
'Legacy Integrations',
71+
]);
5372

54-
return getNavigationConfiguration({
73+
const sections = getNavigationConfiguration({
5574
debugFilesNeedsReview: false,
5675
organization,
5776
project,
5877
})
59-
.map(section => ({
60-
icon: <ProjectAvatar project={project} size={16} />,
61-
label: section.name,
62-
items: section.items
63-
.filter(item => shouldShowItem(item, context, section))
64-
.map(item => ({
65-
display: {
66-
label: item.title,
67-
},
68-
keywords: [section.name, 'project settings', 'settings'],
69-
to: replaceRouterParams(item.path, {
70-
orgId: organization.slug,
71-
projectId: project.slug,
72-
}),
73-
})),
74-
}))
78+
.map(section => {
79+
const label =
80+
section.name === 'Project'
81+
? 'General'
82+
: section.name === 'SDK Setup'
83+
? 'SDK setup'
84+
: section.name;
85+
86+
return {
87+
icon: groupedSectionLabels.has(label) ? (
88+
label === 'General' ? (
89+
<IconProject />
90+
) : label === 'Processing' ? (
91+
<IconStack />
92+
) : label === 'SDK setup' ? (
93+
<IconCode />
94+
) : undefined
95+
) : (
96+
<ProjectAvatar project={project} size={16} />
97+
),
98+
label,
99+
items: section.items
100+
.filter(item => shouldShowItem(item, context, section))
101+
.map(item => ({
102+
display: {
103+
label: item.title,
104+
},
105+
keywords: [section.name, 'project settings', 'settings'],
106+
to: replaceRouterParams(item.path, {
107+
orgId: organization.slug,
108+
projectId: project.slug,
109+
}),
110+
})),
111+
};
112+
})
75113
.filter(section => section.items.length > 0);
114+
const groupedSections = sections.filter(section =>
115+
groupedSectionLabels.has(section.label)
116+
);
117+
const ungroupedSections = sections.filter(
118+
section => !groupedSectionLabels.has(section.label)
119+
);
120+
121+
if (groupedSections.length === 0) {
122+
return ungroupedSections;
123+
}
124+
125+
return [
126+
{
127+
icon: <ProjectAvatar project={project} size={16} />,
128+
label: 'Project Settings',
129+
items: groupedSections.map(section =>
130+
section.label === 'Legacy Integrations' && section.items.length > 0
131+
? section.items[0]!
132+
: section
133+
),
134+
},
135+
...ungroupedSections,
136+
];
76137
}
77138

78139
export function ProjectSettingsCommandPaletteActions({
@@ -86,15 +147,30 @@ export function ProjectSettingsCommandPaletteActions({
86147

87148
return (
88149
<Fragment>
89-
{sections.map(section => (
90-
<CommandPaletteSlot key={section.label} name="page">
91-
<CMDKAction display={{label: section.label, icon: section.icon}}>
92-
{section.items.map(item => (
93-
<CMDKAction key={item.to} {...item} />
94-
))}
95-
</CMDKAction>
96-
</CommandPaletteSlot>
97-
))}
150+
{sections.map(section => {
151+
return (
152+
<CommandPaletteSlot key={section.label} name="page">
153+
<CMDKAction display={{label: section.label, icon: section.icon}}>
154+
{section.items.map(item => {
155+
if ('items' in item) {
156+
return (
157+
<CMDKAction
158+
key={item.label}
159+
display={{label: item.label, icon: item.icon}}
160+
>
161+
{item.items.map(action => (
162+
<CMDKAction key={action.to} {...action} />
163+
))}
164+
</CMDKAction>
165+
);
166+
}
167+
168+
return <CMDKAction key={item.to} {...item} />;
169+
})}
170+
</CMDKAction>
171+
</CommandPaletteSlot>
172+
);
173+
})}
98174
</Fragment>
99175
);
100176
}

0 commit comments

Comments
 (0)