Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion static/app/components/customResolutionModal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import {ReleaseFixture} from 'sentry-fixture/release';
import {UserFixture} from 'sentry-fixture/user';

import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';

import {makeCloseButton} from '@sentry/scraps/modal';

Expand Down Expand Up @@ -190,4 +190,55 @@ describe('CustomResolutionModal', () => {
});
expect(matches).toHaveLength(1);
});

it('keeps showing a selected exact-match release after search is cleared', async () => {
MockApiClient.addMockResponse({
url: `/organizations/org-slug/releases/${encodeURIComponent('ancient-release')}/`,
body: ReleaseFixture({
version: 'ancient-release',
versionInfo: {
buildHash: null,
description: 'ancient-release',
package: '',
version: {
raw: 'ancient-release',
},
},
}),
});

render(
<CustomResolutionModal
Header={p => <span>{p.children}</span>}
Body={wrapper()}
Footer={wrapper()}
projectSlug="project-slug"
onSelected={jest.fn()}
closeModal={jest.fn()}
CloseButton={makeCloseButton(() => null)}
/>
);

const trigger = screen.getByRole('button', {name: /version/i});
await userEvent.click(trigger);
const searchInput = await screen.findByRole('textbox');
await userEvent.click(searchInput);
await userEvent.paste('ancient-release');

expect(
await screen.findByRole('option', {name: /ancient-release/})
).toBeInTheDocument();
await userEvent.keyboard('{ArrowDown}');
await userEvent.keyboard('{Enter}');

await waitFor(() => expect(screen.queryByRole('listbox')).not.toBeInTheDocument());

await waitFor(
() =>
expect(screen.getByRole('button', {name: /version/i})).toHaveTextContent(
'ancient-release'
),
{timeout: 600}
);
});
});
40 changes: 27 additions & 13 deletions static/app/components/customResolutionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ function makeReleaseOption(
};
}

function getUniqueReleases(releases: Array<Release | null | undefined>): Release[] {
const seen = new Set<string>();

return releases.filter((release): release is Release => {
if (!release || seen.has(release.version)) {
return false;
}

seen.add(release.version);
return true;
});
}

interface CustomResolutionModalProps extends ModalRenderProps {
onSelected: (change: {inRelease: string}) => void;
projectSlug: string | undefined;
Expand All @@ -62,6 +75,7 @@ interface CustomResolutionModalProps extends ModalRenderProps {
export function CustomResolutionModal(props: CustomResolutionModalProps) {
const organization = useOrganization();
const [version, setVersion] = useState('');
const [selectedRelease, setSelectedRelease] = useState<Release | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const debouncedSearch = useDebouncedValue(searchQuery);
const currentUser = ConfigStore.get('user');
Expand Down Expand Up @@ -109,20 +123,14 @@ export function CustomResolutionModal(props: CustomResolutionModalProps) {
});

const options = useMemo((): Array<SelectOption<string>> => {
const baseOptions = releases.map(release =>
const prioritizedReleases = debouncedSearch.trim()
? [exactRelease, ...releases]
: [selectedRelease, ...releases];

return getUniqueReleases(prioritizedReleases).map(release =>
makeReleaseOption(release, currentUser?.email)
);

if (exactRelease) {
const exactOption = makeReleaseOption(exactRelease, currentUser?.email);

const filtered = baseOptions.filter(opt => opt.value !== exactOption.value);
filtered.unshift(exactOption);
return filtered;
}

return baseOptions;
}, [currentUser?.email, exactRelease, releases]);
}, [currentUser?.email, debouncedSearch, exactRelease, releases, selectedRelease]);

const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
Expand Down Expand Up @@ -157,7 +165,13 @@ export function CustomResolutionModal(props: CustomResolutionModalProps) {
loading={isFetching}
emptyMessage={isFetching ? t('Loading releases\u2026') : t('No releases found')}
onChange={option => {
setVersion(option?.value ? String(option.value) : '');
const selectedVersion = option?.value ? String(option.value) : '';
const visibleReleases = getUniqueReleases([exactRelease, ...releases]);
const release =
visibleReleases.find(item => item.version === selectedVersion) ?? null;

setVersion(selectedVersion);
setSelectedRelease(release);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onChange drops selectedRelease lookup

Medium Severity

The options list keeps a search-only release via selectedRelease when the debounced search is empty, but onChange resolves the chosen release only from exactRelease and the current releases fetch. If the user interacts with that option again after another search (or cached exact lookup no longer matches), lookup fails and selectedRelease is cleared while version stays set, so the Version trigger can show None again.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0daee14. Configure here.

setSelectionError(null);
setSearchQuery('');
}}
Expand Down
Loading