Skip to content

Commit d4df676

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 d4df676

File tree

2 files changed

+139
-44
lines changed

2 files changed

+139
-44
lines changed

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

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,32 @@ describe('ProjectSettingsCommandPaletteActions', () => {
2323
expect(sections).toEqual(
2424
expect.arrayContaining([
2525
expect.objectContaining({
26-
label: 'Project',
26+
label: 'Project Settings',
2727
items: expect.arrayContaining([
2828
expect.objectContaining({
29-
display: expect.objectContaining({label: 'General Settings'}),
30-
to: '/settings/acme/projects/frontend/',
29+
label: 'General',
30+
items: expect.arrayContaining([
31+
expect.objectContaining({
32+
display: expect.objectContaining({label: 'General Settings'}),
33+
to: '/settings/acme/projects/frontend/',
34+
}),
35+
]),
3136
}),
32-
]),
33-
}),
34-
expect.objectContaining({
35-
label: 'Processing',
36-
items: expect.arrayContaining([
3737
expect.objectContaining({
38-
display: expect.objectContaining({label: 'Performance'}),
39-
to: '/settings/acme/projects/frontend/performance/',
38+
label: 'Processing',
39+
items: expect.arrayContaining([
40+
expect.objectContaining({
41+
display: expect.objectContaining({label: 'Performance'}),
42+
to: '/settings/acme/projects/frontend/performance/',
43+
}),
44+
]),
45+
}),
46+
expect.objectContaining({
47+
label: 'SDK setup',
4048
}),
41-
]),
42-
}),
43-
expect.objectContaining({
44-
label: 'Legacy Integrations',
45-
items: expect.arrayContaining([
4649
expect.objectContaining({
47-
display: expect.objectContaining({label: 'GitHub'}),
48-
to: '/settings/acme/projects/frontend/plugins/github/',
50+
display: expect.objectContaining({label: 'Legacy Integrations'}),
51+
to: '/settings/acme/projects/frontend/plugins/',
4952
}),
5053
]),
5154
}),
@@ -55,8 +58,16 @@ describe('ProjectSettingsCommandPaletteActions', () => {
5558
expect(sections).not.toEqual(
5659
expect.arrayContaining([
5760
expect.objectContaining({
61+
label: 'Project Settings',
5862
items: expect.arrayContaining([
59-
expect.objectContaining({to: '/settings/acme/projects/frontend/replays/'}),
63+
expect.objectContaining({
64+
label: 'Processing',
65+
items: expect.arrayContaining([
66+
expect.objectContaining({
67+
to: '/settings/acme/projects/frontend/replays/',
68+
}),
69+
]),
70+
}),
6071
]),
6172
}),
6273
expect.objectContaining({

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

Lines changed: 110 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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';
@@ -19,12 +20,66 @@ type ProjectSettingsCommandPaletteAction = {
1920
to: string;
2021
};
2122

22-
type ProjectSettingsCommandPaletteSection = {
23-
icon: ReactNode;
24-
items: ProjectSettingsCommandPaletteAction[];
23+
type ProjectSettingsCommandPaletteNode = {
24+
items: Array<ProjectSettingsCommandPaletteAction | ProjectSettingsCommandPaletteNode>;
2525
label: string;
26+
icon?: ReactNode;
2627
};
2728

29+
function getProjectSettingsSectionLabel(sectionName: string) {
30+
switch (sectionName) {
31+
case 'Project':
32+
return 'General';
33+
case 'SDK Setup':
34+
return 'SDK setup';
35+
default:
36+
return sectionName;
37+
}
38+
}
39+
40+
function getProjectSettingsSectionIcon(sectionLabel: string): ReactNode | undefined {
41+
switch (sectionLabel) {
42+
case 'General':
43+
return <IconProject />;
44+
case 'Processing':
45+
return <IconStack />;
46+
case 'SDK setup':
47+
return <IconCode />;
48+
default:
49+
return undefined;
50+
}
51+
}
52+
53+
function flattenSingleItemSection(
54+
section: ProjectSettingsCommandPaletteNode
55+
): ProjectSettingsCommandPaletteNode | ProjectSettingsCommandPaletteAction {
56+
if (section.label === 'Legacy Integrations' && section.items.length > 0) {
57+
return section.items[0]!;
58+
}
59+
60+
return section;
61+
}
62+
63+
function isProjectSettingsCommandPaletteNode(
64+
item: ProjectSettingsCommandPaletteAction | ProjectSettingsCommandPaletteNode
65+
): item is ProjectSettingsCommandPaletteNode {
66+
return 'items' in item;
67+
}
68+
69+
function renderProjectSettingsCommandPaletteNode(
70+
node: ProjectSettingsCommandPaletteNode | ProjectSettingsCommandPaletteAction
71+
): React.ReactNode {
72+
if (!isProjectSettingsCommandPaletteNode(node)) {
73+
return <CMDKAction key={node.to} {...node} />;
74+
}
75+
76+
return (
77+
<CMDKAction key={node.label} display={{label: node.label, icon: node.icon}}>
78+
{node.items.map(item => renderProjectSettingsCommandPaletteNode(item))}
79+
</CMDKAction>
80+
);
81+
}
82+
2883
function shouldShowItem(
2984
item: NavigationItem,
3085
context: Omit<NavigationGroupProps, 'items' | 'name' | 'id'>,
@@ -43,36 +98,69 @@ export function getProjectSettingsCommandPaletteSections({
4398
}: {
4499
organization: Organization;
45100
project: Project;
46-
}): ProjectSettingsCommandPaletteSection[] {
101+
}): ProjectSettingsCommandPaletteNode[] {
47102
const context = {
48103
access: new Set(organization.access),
49104
features: new Set(organization.features),
50105
organization,
51106
project,
52107
};
108+
const groupedSectionLabels = new Set([
109+
'General',
110+
'Processing',
111+
'SDK setup',
112+
'Legacy Integrations',
113+
]);
53114

54-
return getNavigationConfiguration({
115+
const sections = getNavigationConfiguration({
55116
debugFilesNeedsReview: false,
56117
organization,
57118
project,
58119
})
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-
}))
120+
.map(section => {
121+
const label = getProjectSettingsSectionLabel(section.name);
122+
123+
return {
124+
icon: groupedSectionLabels.has(label) ? (
125+
getProjectSettingsSectionIcon(label)
126+
) : (
127+
<ProjectAvatar project={project} size={16} />
128+
),
129+
label,
130+
items: section.items
131+
.filter(item => shouldShowItem(item, context, section))
132+
.map(item => ({
133+
display: {
134+
label: item.title,
135+
},
136+
keywords: [section.name, 'project settings', 'settings'],
137+
to: replaceRouterParams(item.path, {
138+
orgId: organization.slug,
139+
projectId: project.slug,
140+
}),
141+
})),
142+
};
143+
})
75144
.filter(section => section.items.length > 0);
145+
const groupedSections = sections.filter(section =>
146+
groupedSectionLabels.has(section.label)
147+
);
148+
const ungroupedSections = sections.filter(
149+
section => !groupedSectionLabels.has(section.label)
150+
);
151+
152+
if (groupedSections.length === 0) {
153+
return ungroupedSections;
154+
}
155+
156+
return [
157+
{
158+
icon: <ProjectAvatar project={project} size={16} />,
159+
label: 'Project Settings',
160+
items: groupedSections.map(flattenSingleItemSection),
161+
},
162+
...ungroupedSections,
163+
];
76164
}
77165

78166
export function ProjectSettingsCommandPaletteActions({
@@ -88,11 +176,7 @@ export function ProjectSettingsCommandPaletteActions({
88176
<Fragment>
89177
{sections.map(section => (
90178
<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>
179+
{renderProjectSettingsCommandPaletteNode(section)}
96180
</CommandPaletteSlot>
97181
))}
98182
</Fragment>

0 commit comments

Comments
 (0)