Skip to content

Commit 7508699

Browse files
JonasBaclaudecursoragent
authored
feat(pagefilters): Fix sentinel toggle behavior for All/My Projects quick-select (#109545)
Updates hybrid filters with My Projects and All Projects selection mechanism --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jonas <JonasBa@users.noreply.github.com>
1 parent 26e3bef commit 7508699

File tree

9 files changed

+1194
-376
lines changed

9 files changed

+1194
-376
lines changed

static/app/components/core/compactSelect/compactSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export function CompactSelect<Value extends SelectKey>({
123123
const [measuredMenuWidth, setMeasuredMenuWidth] = useState<number>();
124124
const [hasMeasured, setHasMeasured] = useState(false);
125125
const needsMeasuring =
126-
!menuWidth && !grid && !hasMeasured && shouldVirtualize(options, virtualizeThreshold);
126+
!menuWidth && !hasMeasured && shouldVirtualize(options, virtualizeThreshold);
127127

128128
const menuRef = useCallback(
129129
(element: HTMLDivElement | null) => {

static/app/components/core/compactSelect/gridList/index.tsx

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import {
1212
ListWrap,
1313
SelectFilterContext,
1414
SizeLimitMessage,
15+
useVirtualizedItems,
1516
type SelectKey,
1617
type SelectSection,
1718
} from '@sentry/scraps/compactSelect';
19+
import {Container} from '@sentry/scraps/layout';
1820

1921
import {t} from 'sentry/locale';
2022

@@ -63,6 +65,10 @@ interface GridListProps
6365
* Message to be displayed when some options are hidden due to `sizeLimit`.
6466
*/
6567
sizeLimitMessage?: string;
68+
/**
69+
* If true, virtualization will be enabled for the list.
70+
*/
71+
virtualized?: boolean;
6672
}
6773

6874
/**
@@ -82,6 +88,7 @@ function GridList({
8288
onSectionToggle,
8389
sizeLimitMessage,
8490
keyDownHandler,
91+
virtualized,
8592
...props
8693
}: GridListProps) {
8794
const ref = useRef<HTMLUListElement>(null);
@@ -114,41 +121,61 @@ function GridList({
114121
[listState.collection, hiddenOptions]
115122
);
116123

124+
const virtualizer = useVirtualizedItems({
125+
listItems,
126+
virtualized,
127+
size,
128+
});
129+
130+
const mergedProps = mergeProps(gridProps, props);
131+
117132
return (
118133
<Fragment>
119134
{listItems.length !== 0 && <ListSeparator role="separator" />}
120135
{listItems.length !== 0 && label && <ListLabel id={labelId}>{label}</ListLabel>}
121136
{overlayIsOpen && (
122-
<ListWrap {...mergeProps(gridProps, props)} onKeyDown={onKeyDown} ref={ref}>
123-
{listItems.map(item => {
124-
if (item.type === 'section') {
125-
return (
126-
<GridListSection
127-
key={item.key}
128-
node={item}
129-
listState={listState}
130-
onToggle={onSectionToggle}
131-
size={size}
132-
/>
133-
);
134-
}
137+
<Container ref={virtualizer.scrollElementRef} height="100%" overflowY="auto">
138+
<Container {...virtualizer.wrapperProps}>
139+
<ListWrap
140+
{...mergedProps}
141+
style={{...mergedProps.style, ...virtualizer.listWrapStyle}}
142+
onKeyDown={onKeyDown}
143+
ref={ref}
144+
>
145+
{virtualizer.items.map(row => {
146+
const item = listItems[row.index]!;
147+
if (item.type === 'section') {
148+
return (
149+
<GridListSection
150+
{...virtualizer.itemProps(row.index)}
151+
key={item.key}
152+
node={item}
153+
listState={listState}
154+
onToggle={onSectionToggle}
155+
size={size}
156+
/>
157+
);
158+
}
135159

136-
return (
137-
<GridListOption
138-
key={item.key}
139-
node={item}
140-
listState={listState}
141-
size={size}
142-
/>
143-
);
144-
})}
160+
return (
161+
<GridListOption
162+
key={item.key}
163+
{...virtualizer.itemProps(row.index)}
164+
node={item}
165+
listState={listState}
166+
size={size}
167+
/>
168+
);
169+
})}
145170

146-
{!searchable && hiddenOptions.size > 0 && (
147-
<SizeLimitMessage>
148-
{sizeLimitMessage ?? t('Use search to find more options…')}
149-
</SizeLimitMessage>
150-
)}
151-
</ListWrap>
171+
{!searchable && hiddenOptions.size > 0 && (
172+
<SizeLimitMessage>
173+
{sizeLimitMessage ?? t('Use search to find more options…')}
174+
</SizeLimitMessage>
175+
)}
176+
</ListWrap>
177+
</Container>
178+
</Container>
152179
)}
153180
</Fragment>
154181
);

static/app/components/core/compactSelect/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export {
4444
getEscapedKey,
4545
} from './utils';
4646

47+
export {useVirtualizedItems} from './useVirtualizedItems';
48+
4749
export {SelectFilterContext} from './list';
4850
export {TriggerLabel} from './control';
4951

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {useRef} from 'react';
2+
import type {Node} from '@react-types/shared';
3+
import {useVirtualizer} from '@tanstack/react-virtual';
4+
5+
import type {FormSize} from 'sentry/utils/theme';
6+
7+
const heightEstimations = {
8+
sm: {regular: 32, large: 49},
9+
md: {regular: 36, large: 53},
10+
xs: {regular: 25, large: 42},
11+
} as const satisfies Record<FormSize, {large: number; regular: number}>;
12+
13+
// explicitly using object here because Record<PropertyKey, unknown> requires an index signature
14+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
15+
type ObjectLike = object;
16+
17+
export function useVirtualizedItems<T extends ObjectLike>({
18+
listItems,
19+
virtualized = false,
20+
size,
21+
}: {
22+
listItems: Array<Node<T>>;
23+
size: FormSize;
24+
virtualized: boolean | undefined;
25+
}) {
26+
const scrollElementRef = useRef<HTMLDivElement>(null);
27+
const heightEstimation = heightEstimations[size];
28+
29+
const virtualizer = useVirtualizer({
30+
count: listItems.length,
31+
getScrollElement: () => scrollElementRef?.current,
32+
estimateSize: index => {
33+
const item = listItems[index];
34+
if (item?.value && 'details' in item.value) {
35+
return heightEstimation.large;
36+
}
37+
return heightEstimation.regular;
38+
},
39+
enabled: virtualized,
40+
});
41+
42+
if (virtualized) {
43+
const virtualizedItems = virtualizer.getVirtualItems();
44+
return {
45+
items: virtualizedItems,
46+
scrollElementRef,
47+
itemProps: (index: number) => ({
48+
ref: virtualizer.measureElement,
49+
'data-index': index,
50+
}),
51+
wrapperProps: {
52+
'data-is-virtualized': true,
53+
style: {
54+
height: virtualizer.getTotalSize(),
55+
width: '100%',
56+
position: 'relative',
57+
},
58+
},
59+
listWrapStyle: {
60+
position: 'absolute',
61+
top: 0,
62+
left: 0,
63+
width: '100%',
64+
transform: `translateY(${virtualizedItems[0]?.start ?? 0}px)`,
65+
},
66+
} as const;
67+
}
68+
69+
return {
70+
items: listItems.map((_, index) => ({index, start: 0})),
71+
scrollElementRef: undefined,
72+
itemProps: () => undefined,
73+
wrapperProps: {
74+
'data-is-virtualized': false,
75+
},
76+
listWrapStyle: {},
77+
} as const;
78+
}

static/app/components/pageFilters/environment/environmentPageFilter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,11 @@ export function EnvironmentPageFilter({
259259
<Flex gap="md" align="center" justify="end">
260260
<MenuComponents.CancelButton
261261
disabled={!hasStagedChanges}
262-
onClick={() => dispatch({type: 'remove staged'})}
262+
onClick={() => stagedSelect.dispatch({type: 'remove staged'})}
263263
/>
264264
<MenuComponents.ApplyButton
265265
onClick={() => {
266-
dispatch({type: 'remove staged'});
266+
stagedSelect.dispatch({type: 'remove staged'});
267267
handleChange(stagedSelect.value);
268268
}}
269269
/>

0 commit comments

Comments
 (0)