Skip to content

Commit f8e8949

Browse files
feat(ui): Add grouping capability to sentryProjectSelectorField (#69046)
Can be used to do things like this <img width="292" alt="image" src="https://github.com/getsentry/sentry/assets/1421724/123a8840-8f87-474b-bf84-540e5c5a451f">
1 parent 09b4607 commit f8e8949

File tree

2 files changed

+76
-12
lines changed

2 files changed

+76
-12
lines changed

static/app/components/forms/fields/sentryProjectSelectorField.spec.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,37 @@ describe('SentryProjectSelectorField', () => {
2424

2525
expect(mock).toHaveBeenCalledWith('23', expect.anything());
2626
});
27+
28+
it('can group values', async () => {
29+
const mock = jest.fn();
30+
const projects = [
31+
ProjectFixture(),
32+
ProjectFixture({
33+
id: '23',
34+
slug: 'my-proj',
35+
name: 'My Proj',
36+
}),
37+
ProjectFixture({
38+
id: '24',
39+
slug: 'other-project',
40+
name: 'My Other Project',
41+
}),
42+
];
43+
render(
44+
<SentryProjectSelectorField
45+
groupProjects={project => (project.slug === 'other-project' ? 'other' : 'main')}
46+
groupLabels={{
47+
main: 'Main projects',
48+
}}
49+
onChange={mock}
50+
name="project"
51+
projects={projects}
52+
/>
53+
);
54+
55+
await selectEvent.openMenu(screen.getByText(/choose sentry project/i));
56+
57+
expect(screen.getByText('Main projects')).toBeInTheDocument();
58+
expect(screen.getByText('other')).toBeInTheDocument();
59+
});
2760
});

static/app/components/forms/fields/sentryProjectSelectorField.tsx

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import groupBy from 'lodash/groupBy';
2+
13
import IdBadge from 'sentry/components/idBadge';
24
import {t} from 'sentry/locale';
35
import type {Project} from 'sentry/types';
@@ -6,9 +8,24 @@ import type {Project} from 'sentry/types';
68
import type {InputFieldProps} from './inputField';
79
import SelectField from './selectField';
810

11+
/**
12+
* Function used to group projects by returning the key of the group
13+
*/
14+
type GroupProjects = (project: Project) => string;
15+
916
// projects can be passed as a direct prop as well
1017
export interface RenderFieldProps extends InputFieldProps {
1118
avatarSize?: number;
19+
/**
20+
* When using groupProjects you can specify the labels of the groups as a
21+
* mapping of the key returned for the groupProjects to the label
22+
*/
23+
groupLabels?: Record<string, React.ReactNode>;
24+
/**
25+
* Controls grouping of projects within the field. Useful to prioritize some
26+
* projects above others
27+
*/
28+
groupProjects?: GroupProjects;
1229
projects?: Project[];
1330
/**
1431
* Use the slug as the select field value. Without setting this the numeric id
@@ -19,23 +36,37 @@ export interface RenderFieldProps extends InputFieldProps {
1936

2037
function SentryProjectSelectorField({
2138
projects,
39+
groupProjects,
40+
groupLabels,
2241
avatarSize = 20,
2342
placeholder = t('Choose Sentry project'),
2443
valueIsSlug,
2544
...props
2645
}: RenderFieldProps) {
27-
const projectOptions = projects?.map(project => ({
28-
value: project[valueIsSlug ? 'slug' : 'id'],
29-
label: project.slug,
30-
leadingItems: (
31-
<IdBadge
32-
project={project}
33-
avatarSize={avatarSize}
34-
avatarProps={{consistentWidth: true}}
35-
hideName
36-
/>
37-
),
38-
}));
46+
function projectToOption(project: Project) {
47+
return {
48+
value: project[valueIsSlug ? 'slug' : 'id'],
49+
label: project.slug,
50+
leadingItems: (
51+
<IdBadge
52+
project={project}
53+
avatarSize={avatarSize}
54+
avatarProps={{consistentWidth: true}}
55+
hideName
56+
/>
57+
),
58+
};
59+
}
60+
61+
const projectOptions =
62+
projects && groupProjects
63+
? // Create project groups when groupProjects is in use
64+
Object.entries(groupBy(projects, groupProjects)).map(([key, projectsGroup]) => ({
65+
label: groupLabels?.[key] ?? key,
66+
options: projectsGroup.map(projectToOption),
67+
}))
68+
: // Otherwise just map projects to the options
69+
projects?.map(projectToOption);
3970

4071
return <SelectField placeholder={placeholder} options={projectOptions} {...props} />;
4172
}

0 commit comments

Comments
 (0)